-
Notifications
You must be signed in to change notification settings - Fork 19
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(web): rewrite api to use hono (#103)
- Loading branch information
Showing
32 changed files
with
1,903 additions
and
864 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" }); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); |
Oops, something went wrong.
390ed0f
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Successfully deployed to the following URLs:
abby-opensource – ./apps/web
abby-opensource-dynabase.vercel.app
preview.tryabby.com
abby-opensource-git-main-dynabase.vercel.app
abby-opensource.vercel.app
390ed0f
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Successfully deployed to the following URLs:
abby-docs – ./apps/docs
abby-docs-git-main-dynabase.vercel.app
abby-docs-dynabase.vercel.app
abby-docs.vercel.app
docs.tryabby.dev
docs.tryabby.com