Skip to content

Commit

Permalink
Automatically create bottles from reviews (#126)
Browse files Browse the repository at this point in the history
  • Loading branch information
dcramer authored Dec 31, 2023
1 parent ee1e945 commit 005da7a
Show file tree
Hide file tree
Showing 4 changed files with 144 additions and 3 deletions.
13 changes: 12 additions & 1 deletion apps/server/src/lib/bottleFinder.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ilike, sql } from "drizzle-orm";
import { db } from "../db";
import { bottleAliases, bottles } from "../db/schema";
import { bottleAliases, bottles, entities, type Entity } from "../db/schema";

export async function findBottleId(name: string): Promise<number | null> {
let result: { id: number | null } | null | undefined;
Expand Down Expand Up @@ -36,3 +36,14 @@ export async function findBottleId(name: string): Promise<number | null> {

return result?.id || null;
}

export async function findEntity(fullName: string): Promise<Entity | null> {
const [result] = await db
.select()
.from(entities)
.where(sql`${entities.name} ~* ANY (string_to_array(${fullName}, ' '))`)
.orderBy(sql`LENGTH(${entities.name})`)
.limit(1);

return result ?? null;
}
2 changes: 2 additions & 0 deletions apps/server/src/schemas/reviews.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { z } from "zod";
import { CategoryEnum } from "./bottles";
import { ExternalSiteSchema, ExternalSiteTypeEnum } from "./externalSites";

export const ReviewSchema = z.object({
Expand All @@ -13,6 +14,7 @@ export const ReviewSchema = z.object({
export const ReviewInputSchema = z.object({
site: ExternalSiteTypeEnum,
name: z.string().trim().min(1, "Required"),
category: CategoryEnum.nullable().optional(),
rating: z.number(),
issue: z.string().trim().min(1, "Required"),
url: z.string().trim().min(1, "Required"),
Expand Down
114 changes: 114 additions & 0 deletions apps/server/src/trpc/routes/reviewCreate.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { db } from "@peated/server/db";
import * as Fixtures from "../../lib/test/fixtures";
import { createCaller } from "../router";

test("requires admin", async () => {
const site = await Fixtures.ExternalSite();

const caller = createCaller({
user: await Fixtures.User({ mod: true }),
});

expect(() =>
caller.reviewCreate({
site: site.type,
name: "Bottle Name",
issue: "Default",
rating: 89,
url: "https://example.com",
category: "single_malt",
}),
).rejects.toThrowError(/UNAUTHORIZED/);
});

test("new review with new bottle no entity", async () => {
const site = await Fixtures.ExternalSite();

const caller = createCaller({
user: await Fixtures.User({ admin: true }),
});

const data = await caller.reviewCreate({
site: site.type,
name: "Bottle Name",
issue: "Default",
rating: 89,
url: "https://example.com",
category: "single_malt",
});

const review = await db.query.reviews.findFirst({
where: (table, { eq }) => eq(table.id, data.id),
});
expect(review).toBeDefined();
expect(review?.bottleId).toBeNull();
expect(review?.name).toEqual("Bottle Name");
expect(review?.issue).toEqual("Default");
expect(review?.rating).toEqual(89);
expect(review?.url).toEqual("https://example.com");
});

test("new review with new bottle matching entity", async () => {
const site = await Fixtures.ExternalSite();
const brand = await Fixtures.Entity();

const caller = createCaller({
user: await Fixtures.User({ admin: true }),
});

const data = await caller.reviewCreate({
site: site.type,
name: `${brand.name} Bottle Name`,
issue: "Default",
rating: 89,
url: "https://example.com",
category: "single_malt",
});

const review = await db.query.reviews.findFirst({
where: (table, { eq }) => eq(table.id, data.id),
});
expect(review).toBeDefined();
expect(review?.bottleId).toBeTruthy();
expect(review?.name).toEqual(`${brand.name} Bottle Name`);
expect(review?.issue).toEqual("Default");
expect(review?.rating).toEqual(89);
expect(review?.url).toEqual("https://example.com");

const bottle = await db.query.bottles.findFirst({
where: (table, { eq }) => eq(table.id, review!.bottleId as number),
});
expect(bottle).toBeDefined();
expect(bottle?.fullName).toEqual(`${brand.name} Bottle Name`);
expect(bottle?.name).toEqual("Bottle Name");
expect(bottle?.category).toEqual("single_malt");
expect(bottle?.brandId).toEqual(brand.id);
});

test("new review with existing bottle", async () => {
const site = await Fixtures.ExternalSite();
const bottle = await Fixtures.Bottle();

const caller = createCaller({
user: await Fixtures.User({ admin: true }),
});

const data = await caller.reviewCreate({
site: site.type,
name: bottle.fullName,
issue: "Default",
rating: 89,
url: "https://example.com",
category: bottle.category,
});

const review = await db.query.reviews.findFirst({
where: (table, { eq }) => eq(table.id, data.id),
});
expect(review).toBeDefined();
expect(review?.bottleId).toEqual(bottle.id);
expect(review?.name).toEqual(bottle.fullName);
expect(review?.issue).toEqual("Default");
expect(review?.rating).toEqual(89);
expect(review?.url).toEqual("https://example.com");
});
18 changes: 16 additions & 2 deletions apps/server/src/trpc/routes/reviewCreate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ import {
externalSites,
reviews,
} from "@peated/server/db/schema";
import { findBottleId } from "@peated/server/lib/bottleFinder";
import { findBottleId, findEntity } from "@peated/server/lib/bottleFinder";
import { ReviewInputSchema } from "@peated/server/schemas";
import { serialize } from "@peated/server/serializers";
import { ReviewSerializer } from "@peated/server/serializers/review";
import { TRPCError } from "@trpc/server";
import { eq, isNull, sql } from "drizzle-orm";
import { adminProcedure } from "..";
import { createCaller } from "../router";

export default adminProcedure
.input(ReviewInputSchema)
Expand All @@ -26,7 +27,20 @@ export default adminProcedure
});
}

const bottleId = await findBottleId(input.name);
let bottleId = await findBottleId(input.name);
if (!bottleId) {
const entity = await findEntity(input.name);
if (entity) {
const caller = createCaller(ctx);
const result = await caller.bottleCreate({
name: input.name,
brand: entity.id,
category: input.category,
});
bottleId = result.id;
}
}

const review = await db.transaction(async (tx) => {
const [review] = await tx
.insert(reviews)
Expand Down

0 comments on commit 005da7a

Please sign in to comment.