Skip to content

Commit

Permalink
Add toast serialization
Browse files Browse the repository at this point in the history
  • Loading branch information
dcramer committed May 15, 2023
1 parent 6200a2f commit 9836afe
Show file tree
Hide file tree
Showing 10 changed files with 218 additions and 67 deletions.
35 changes: 32 additions & 3 deletions apps/api/src/bin/mocks.ts
@@ -1,6 +1,6 @@
import { eq, ne, sql } from "drizzle-orm";
import { db } from "../db";
import { bottles, follows, users } from "../db/schema";
import { bottles, follows, tastings, toasts, users } from "../db/schema";
import * as Fixtures from "../lib/test/fixtures";

import { program } from "commander";
Expand All @@ -15,13 +15,13 @@ program
"--bottles <number>",
"number of bottles",
(v: string) => parseInt(v, 10),
25,
5,
)
.option(
"--tastings <number>",
"number of tastings",
(v: string) => parseInt(v, 10),
25,
5,
)
.action(async (email, options) => {
for (let i = 0; i < options.bottles; i++) {
Expand Down Expand Up @@ -69,6 +69,35 @@ program
});
console.log(`Created follow request from ${fromUserId} -> ${toUserId}`);
}

const [lastTasting] = await db
.insert(tastings)
.values({
bottleId: (
await db
.select()
.from(bottles)
.orderBy(sql`RANDOM()`)
.limit(1)
)[0].id,
rating: 4.5,
createdById: toUserId,
})
.returning();
for (const { id: fromUserId } of userList) {
const toast = await Fixtures.Toast({
tastingId: lastTasting.id,
createdById: fromUserId,
});
await createNotification(db, {
fromUserId: fromUserId,
objectType: objectTypeFromSchema(toasts),
objectId: toast.id,
userId: toUserId,
createdAt: toast.createdAt,
});
console.log(`Created toast from ${fromUserId}`);
}
}
});

Expand Down
26 changes: 26 additions & 0 deletions apps/api/src/lib/serializers/tasting.ts
Expand Up @@ -31,3 +31,29 @@ export const serializeTasting = (
};
return data;
};

export const serializeTastingRef = (
tasting: {
id: number;
bottle: {
id: number;
name: string;
brand: {
id: number;
name: string;
};
};
},
currentUser?: User,
) => {
return {
id: tasting.id,
bottle: {
name: tasting.bottle.name,
brand: {
id: tasting.bottle.brand.id,
name: tasting.bottle.brand.name,
},
},
};
};
22 changes: 18 additions & 4 deletions apps/api/src/routes/deleteTasting.ts
@@ -1,8 +1,9 @@
import { eq } from "drizzle-orm";
import { and, eq, inArray, sql } from "drizzle-orm";
import type { RouteOptions } from "fastify";
import { IncomingMessage, Server, ServerResponse } from "http";
import { db } from "../db";
import { tastings } from "../db/schema";
import { notifications, tastings, toasts } from "../db/schema";
import { objectTypeFromSchema } from "../lib/notifications";
import { requireAuth } from "../middleware/auth";

export default {
Expand Down Expand Up @@ -32,8 +33,21 @@ export default {
return res.status(403).send({ error: "Forbidden" });
}

await db.delete(tastings).where(eq(tastings.id, tasting.id));

await db.transaction(async (tx) => {
await tx
.delete(notifications)
.where(
and(
eq(notifications.objectType, objectTypeFromSchema(toasts)),
inArray(
notifications.objectId,
sql`(SELECT ${toasts.id} FROM ${toasts} WHERE ${toasts.tastingId} = ${tasting.id})`,
),
),
);
await tx.delete(toasts).where(eq(toasts.tastingId, tasting.id));
await tx.delete(tastings).where(eq(tastings.id, tasting.id));
});
res.status(204).send();
},
} as RouteOptions<
Expand Down
44 changes: 33 additions & 11 deletions apps/api/src/routes/listNotifications.ts
Expand Up @@ -3,9 +3,18 @@ import { alias } from "drizzle-orm/pg-core";
import type { RouteOptions } from "fastify";
import { IncomingMessage, Server, ServerResponse } from "http";
import { db } from "../db";
import { follows, notifications, tastings, toasts, users } from "../db/schema";
import {
bottles,
entities,
follows,
notifications,
tastings,
toasts,
users,
} from "../db/schema";
import { buildPageLink } from "../lib/paging";
import { serializeFollow } from "../lib/serializers/follow";
import { serializeTastingRef } from "../lib/serializers/tasting";
import { requireAuth } from "../middleware/auth";

export default {
Expand Down Expand Up @@ -85,18 +94,31 @@ export default {
.filter(({ notification }) => notification.objectType === "toast")
.map(({ notification }) => notification.objectId);
const toastResults = toastIdList.length
? await db
.select({
toastId: toasts.id,
tasting: tastings,
})
.from(tastings)
.innerJoin(toasts, eq(tastings.id, toasts.tastingId))
.where(inArray(toasts.id, toastIdList))
? (
await db
.select({
toastId: toasts.id,
tasting: tastings,
bottle: bottles,
brand: entities,
})
.from(tastings)
.innerJoin(bottles, eq(tastings.bottleId, bottles.id))
.innerJoin(entities, eq(bottles.brandId, entities.id))
.innerJoin(toasts, eq(tastings.id, toasts.tastingId))
.where(inArray(toasts.id, toastIdList))
).map((r) => ({
toastId: r.toastId,
...r.tasting,
bottle: {
...r.bottle,
brand: r.brand,
},
}))
: [];

const toastResultsByObjectId = Object.fromEntries(
toastResults.map((r) => [r.toastId, r.tasting]),
toastResults.map((r) => [r.toastId, r]),
);

res.send({
Expand All @@ -110,7 +132,7 @@ export default {
req.user,
)
: notification.objectType === "toast"
? toastResultsByObjectId[notification.objectId]
? serializeTastingRef(toastResultsByObjectId[notification.objectId])
: undefined,
})),
rel: {
Expand Down
31 changes: 16 additions & 15 deletions apps/api/src/routes/listTastings.ts
Expand Up @@ -102,22 +102,23 @@ export default {
else distillersByBottleId[d.bottleId].push(d.distiller);
});

const userToastsList: number[] = req.user
? (
await db
.select({ tastingId: toasts.tastingId })
.from(toasts)
.where(
and(
inArray(
toasts.tastingId,
results.map((t) => t.tasting.id),
const userToastsList: number[] =
req.user && results.length
? (
await db
.select({ tastingId: toasts.tastingId })
.from(toasts)
.where(
and(
inArray(
toasts.tastingId,
results.map((t) => t.tasting.id),
),
eq(toasts.createdById, req.user.id),
),
eq(toasts.createdById, req.user.id),
),
)
).map((t) => t.tastingId)
: [];
)
).map((t) => t.tastingId)
: [];

res.send({
results: results
Expand Down
72 changes: 57 additions & 15 deletions apps/web/src/components/notifications/entry.tsx
@@ -1,3 +1,6 @@
import { XMarkIcon } from "@heroicons/react/20/solid";
import { Link, useNavigate } from "react-router-dom";
import classNames from "../../lib/classNames";
import { Notification } from "../../types";
import UserAvatar from "../userAvatar";
import FollowEntry from "./followEntry";
Expand All @@ -9,46 +12,85 @@ export default function NotificationEntry({
notification: Notification;
onArchive: () => void;
}) {
const navigate = useNavigate();
const link = getLink({ notification });
return (
<div className="p-4">
<div
className={classNames(
"p-3",
link ? "cursor-pointer hover:bg-gray-100" : "",
)}
onClick={
link
? () => {
navigate(link);
}
: undefined
}
>
<div className="flex flex-1 items-start">
<div className="flex-shrink-0 pt-0.5">
<UserAvatar user={notification.fromUser} size={36} />
<div className="flex-shrink-0 self-center">
<UserAvatar user={notification.fromUser} size={32} />
</div>
<div className="ml-3 flex w-0 flex-1 flex-col gap-y-2">
<div className="ml-3 flex w-0 flex-1 flex-col">
<div className="flex flex-1">
<div className="flex-1">
<p className="text-sm font-medium text-gray-900">
{notification.fromUser?.displayName || "Unknown User"}
</p>
<p className="mt-1 text-sm text-gray-500">
<p className="text-sm">
{notification.fromUser && (
<Link
to={`/users/${notification.fromUser.id}`}
className="mr-1 font-semibold"
>
{notification.fromUser.displayName}
</Link>
)}
{getStatusMessage({ notification })}
</p>
<NotificationEntryRef notification={notification} />
</div>
<div className="flex min-h-full flex-shrink">
<button
onClick={onArchive}
className="hover:text-peated block h-full w-full rounded border-gray-200 bg-white p-3 text-gray-400 hover:bg-gray-200"
className="hover:text-peated block h-full w-full rounded border-gray-200 bg-inherit p-2 px-1 text-gray-400 hover:bg-gray-200"
>
X
<XMarkIcon className="h-6 w-6" />
</button>
</div>
</div>
<div>
<NotificationEntryRef notification={notification} />
</div>
</div>
</div>
</div>
);
}

const getLink = ({ notification }: { notification: Notification }) => {
switch (notification.objectType) {
case "follow":
return `/users/${notification.objectId}`;
case "toast":
return `/users/${notification.ref.id}`;
default:
return null;
}
};

const getStatusMessage = ({ notification }: { notification: Notification }) => {
switch (notification.objectType) {
case "follow":
return <>Wants to follow you</>;
return <>wants to follow you</>;
case "toast":
return <>Toasted your tasting</>;
return (
<>
toasted your
<Link
to={`/tastings/${notification.ref.id}`}
className="mx-1 font-semibold"
>
{notification.ref.bottle.brand.name}
</Link>
tasting
</>
);
default:
return null;
}
Expand Down
31 changes: 18 additions & 13 deletions apps/web/src/components/notifications/followEntry.tsx
Expand Up @@ -42,18 +42,23 @@ export default ({
};

return (
<Button
color="primary"
size="small"
onClick={() => {
if (theirFollowStatus === "pending") {
acceptRequest(ref.id);
} else {
followUser(ref.user.id, myFollowStatus === "none");
}
}}
>
{theirFollowStatus === "pending" ? "Accept" : followLabel(myFollowStatus)}
</Button>
<div className="mt-2">
<Button
color="primary"
size="small"
onClick={(e) => {
e.stopPropagation();
if (theirFollowStatus === "pending") {
acceptRequest(ref.id);
} else {
followUser(ref.user.id, myFollowStatus === "none");
}
}}
>
{theirFollowStatus === "pending"
? "Accept"
: followLabel(myFollowStatus)}
</Button>
</div>
);
};
2 changes: 1 addition & 1 deletion apps/web/src/components/notifications/list.tsx
Expand Up @@ -18,7 +18,7 @@ export default function NotificationList({
const activeValues = values.filter((n) => archiveList.indexOf(n.id) === -1);

return (
<ul role="list" className="divide-y divide-gray-100">
<ul role="list" className="text-peated divide-y divide-gray-200 bg-white">
{activeValues.map((n) => {
return (
<NotificationEntry
Expand Down

0 comments on commit 9836afe

Please sign in to comment.