Skip to content

Commit

Permalink
feat(web): rewrite api to use hono (#103)
Browse files Browse the repository at this point in the history
  • Loading branch information
cstrnt authored Dec 28, 2023
1 parent 631e914 commit 390ed0f
Show file tree
Hide file tree
Showing 32 changed files with 1,903 additions and 864 deletions.
13 changes: 11 additions & 2 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
"db:migrate": "prisma migrate dev",
"generate:coupons": "pnpm ts-node -r tsconfig-paths/register --compiler-options {\\\"module\\\":\\\"CommonJS\\\"} prisma/generateCoupons.ts",
"mailhog:up": "docker-compose -f docker-compose.mailhog.yaml up",
"mailhog:down": "docker-compose -f docker-compose.mailhog.yaml down"
"mailhog:down": "docker-compose -f docker-compose.mailhog.yaml down",
"test": "vitest"
},
"dependencies": {
"@code-hike/mdx": "0.9.0",
Expand All @@ -24,6 +25,8 @@
"@dnd-kit/utilities": "^3.2.1",
"@fontsource/martian-mono": "^5.0.8",
"@headlessui/react": "^1.7.13",
"@hono/node-server": "^1.3.3",
"@hono/zod-validator": "^0.1.11",
"@mdx-js/loader": "^3.0.0",
"@mdx-js/react": "^3.0.0",
"@monaco-editor/react": "^4.5.1",
Expand Down Expand Up @@ -79,6 +82,7 @@
"csstype": "^3.1.2",
"dayjs": "^1.11.7",
"framer-motion": "^10.12.7",
"hono": "^3.11.8",
"immer": "^9.0.21",
"ioredis": "^5.3.1",
"isbot": "^3.6.8",
Expand Down Expand Up @@ -116,6 +120,7 @@
"@next/bundle-analyzer": "13.3.4",
"@tailwindcss/forms": "^0.5.3",
"@tailwindcss/typography": "^0.5.9",
"@testing-library/react": "^13.4.0",
"@types/lodash-es": "^4.17.7",
"@types/ms": "^0.7.31",
"@types/node": "^18.15.11",
Expand All @@ -125,17 +130,21 @@
"@types/react-dom": "18.0.05",
"@typescript-eslint/eslint-plugin": "^5.58.0",
"@typescript-eslint/parser": "^5.58.0",
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.14",
"eslint": "^8.38.0",
"eslint-config-next": "13.0.2",
"jsdom": "^20.0.3",
"postcss": "^8.4.21",
"prettier": "^2.8.7",
"prettier-plugin-tailwindcss": "^0.1.13",
"prisma": "5.6.0",
"tailwindcss": "^3.3.1",
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.2.0",
"typescript": "^4.9.5"
"typescript": "^4.9.5",
"vite-tsconfig-paths": "^4.2.2",
"vitest": "^0.33.0"
},
"ct3aMetadata": {
"initVersion": "6.11.1"
Expand Down
29 changes: 29 additions & 0 deletions apps/web/src/api/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { makeConfigRoute } from "api/routes/v1_config";
import { makeProjectDataRoute } from "api/routes/v1_project_data";
import { Hono } from "hono";
import { cors } from "hono/cors";
import { logger } from "hono/logger";
import { makeHealthRoute } from "./routes/health";
import { makeEventRoute } from "./routes/v1_event";
import { makeLegacyProjectDataRoute } from "./routes/legacy_project_data";

export function bootstrapApi() {
const app = new Hono().basePath("/api");

// base middleware
app.use("*", logger());
app.use("*", cors({ origin: "*", maxAge: 86400 }));

app.route("/health", makeHealthRoute());

// legacy routes
app.route("/data", makeEventRoute());
app.route("/dashboard", makeLegacyProjectDataRoute());

// v1 routes
app.route("/v1/config", makeConfigRoute());
app.route("/v1/data", makeProjectDataRoute());
app.route("/v1/track", makeEventRoute());

return app;
}
25 changes: 25 additions & 0 deletions apps/web/src/api/routes/health.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { testClient } from "hono/testing";
import { makeHealthRoute } from "./health";

vi.mock("server/db/client", () => ({
prisma: {
verificationToken: {
count: vi.fn(async () => 1),
},
},
}));

vi.mock("server/db/redis", () => ({
redis: {
get: vi.fn(async () => "test"),
},
}));

it("should work", async () => {
const app = makeHealthRoute();

const res = await testClient(app).index.$get();

expect(res.status).toEqual(200);
expect(await res.json()).toEqual({ status: "ok" });
});
14 changes: 14 additions & 0 deletions apps/web/src/api/routes/health.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Hono } from "hono";
import { prisma } from "server/db/client";
import { redis } from "server/db/redis";

export function makeHealthRoute() {
const app = new Hono().get("/", async (c) => {
await Promise.allSettled([
await prisma.verificationToken.count(),
await redis.get("test"),
]);
return c.json({ status: "ok" });
});
return app;
}
164 changes: 164 additions & 0 deletions apps/web/src/api/routes/legacy_project_data.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import { testClient } from "hono/testing";
import { makeLegacyProjectDataRoute } from "./legacy_project_data";
import { EventService } from "server/services/EventService";
import { jobManager } from "server/queue/Manager";
import { FeatureFlag, FeatureFlagValue, Option, Test } from "@prisma/client";
import { Decimal } from "@prisma/client/runtime/library";
import { trackPlanOverage } from "lib/logsnag";

vi.mock("server/services/EventService", () => ({
EventService: {
getEventsForCurrentPeriod: vi.fn(() => {
return {
events: 0,
is80PercentOfLimit: false,
plan: "PRO",
planLimits: {
eventsPerMonth: 100000,
environments: 100,
flags: 100,
tests: 100,
},
} satisfies Awaited<
ReturnType<typeof EventService.getEventsForCurrentPeriod>
>;
}),
},
}));

vi.mock("../../env/server.mjs", () => ({
env: {},
}));

vi.mock("lib/logsnag", () => ({
trackPlanOverage: vi.fn(),
}));

vi.mock("server/queue/Manager", () => ({
jobManager: {
emit: vi.fn().mockResolvedValue(null),
},
}));

vi.mock("server/db/client", () => ({
prisma: {
featureFlagValue: {
findMany: vi.fn().mockResolvedValue([
{
environmentId: "",
flag: {
name: "First Flag",
type: "BOOLEAN",
},
flagId: "",
id: "",
value: "true",
},
] satisfies Array<FeatureFlagValue & { flag: Pick<FeatureFlag, "name" | "type"> }>),
},
test: {
findMany: vi.fn().mockResolvedValue([
{
id: "",
name: "First Test",
createdAt: new Date(),
projectId: "",
updatedAt: new Date(),
options: [
{
chance: { toNumber: () => 0.25 } as Decimal,
},
{
chance: { toNumber: () => 0.25 } as Decimal,
},
{
chance: { toNumber: () => 0.25 } as Decimal,
},
{
chance: { toNumber: () => 0.25 } as Decimal,
},
],
},
] satisfies Array<
Test & {
options: Array<Pick<Option, "chance">>;
}
>),
},
},
}));

vi.mock("server/db/redis", () => ({
redis: {
get: vi.fn(async () => {}),
incr: vi.fn(async () => {}),
},
}));

afterEach(() => {
vi.clearAllMocks();
});

describe("Get Config", () => {
it("should return the correct config", async () => {
const app = makeLegacyProjectDataRoute();

const res = await testClient(app)[":projectId"].data.$get({
param: {
projectId: "test",
},
query: {
environment: "test",
},
});
expect(res.status).toBe(200);
const data = await res.json();

// typeguard to make test fail if data is not AbbyDataResponse
if ("error" in data) {
throw new Error("Expected data to not have an error key");
}
expect((data as any).error).toBeUndefined();

expect(data.tests).toHaveLength(1);
expect(data.tests?.[0]?.name).toBe("First Test");
expect(data.tests?.[0]?.weights).toEqual([0.25, 0.25, 0.25, 0.25]);

expect(data.flags).toHaveLength(1);
expect(data.flags?.[0]?.name).toBe("First Flag");
expect(data.flags?.[0]?.isEnabled).toBe(true);

expect(vi.mocked(jobManager.emit)).toHaveBeenCalledTimes(1);
expect(vi.mocked(jobManager.emit)).toHaveBeenCalledWith(
"after-data-request",
expect.objectContaining({})
);
});

it("should not return data if the plan limit is reached", async () => {
vi.mocked(EventService.getEventsForCurrentPeriod).mockResolvedValueOnce({
events: 5,
is80PercentOfLimit: false,
plan: "PRO",
planLimits: {
eventsPerMonth: 1,
environments: 100,
flags: 100,
tests: 100,
},
} satisfies Awaited<ReturnType<typeof EventService.getEventsForCurrentPeriod>>);

const app = makeLegacyProjectDataRoute();

const res = await testClient(app)[":projectId"].data.$get({
param: {
projectId: "test",
},
query: {
environment: "test",
},
});
expect(res.status).toBe(429);
expect(trackPlanOverage).toHaveBeenCalledTimes(1);
});
});
Loading

2 comments on commit 390ed0f

@vercel
Copy link

@vercel vercel bot commented on 390ed0f Dec 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vercel
Copy link

@vercel vercel bot commented on 390ed0f Dec 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.