refactor(api): ♻️ Convery more routes to use OpenAPI

This commit is contained in:
Jesse Wierzbinski 2024-08-27 17:40:58 +02:00
parent 1ab1c68d36
commit b0b750c05d
No known key found for this signature in database
6 changed files with 289 additions and 131 deletions

View file

@ -1,10 +1,11 @@
import { apiRoute, applyConfig, auth, handleZodError } from "@/api";
import { zValidator } from "@hono/zod-validator";
import { apiRoute, applyConfig, auth } from "@/api";
import { createRoute } from "@hono/zod-openapi";
import ISO6391 from "iso-639-1";
import { z } from "zod";
import { RolePermissions } from "~/drizzle/schema";
import { Relationship } from "~/packages/database-interface/relationship";
import { User } from "~/packages/database-interface/user";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({
allowedMethods: ["POST"],
@ -41,42 +42,79 @@ export const schemas = {
.default({ reblogs: true, notify: false, languages: [] }),
};
export default apiRoute((app) =>
app.on(
meta.allowedMethods,
meta.route,
zValidator("param", schemas.param, handleZodError),
zValidator("json", schemas.json, handleZodError),
auth(meta.auth, meta.permissions),
async (context) => {
const { id } = context.req.valid("param");
const { user } = context.get("auth");
const { reblogs, notify, languages } = context.req.valid("json");
if (!user) {
return context.json({ error: "Unauthorized" }, 401);
}
const otherUser = await User.fromId(id);
if (!otherUser) {
return context.json({ error: "User not found" }, 404);
}
let relationship = await Relationship.fromOwnerAndSubject(
user,
otherUser,
);
if (!relationship.data.following) {
relationship = await user.followRequest(otherUser, {
reblogs,
notify,
languages,
});
}
return context.json(relationship.toApi());
const route = createRoute({
method: "post",
path: "/api/v1/accounts/{id}/follow",
summary: "Follow user",
description: "Follow a user",
middleware: [auth(meta.auth, meta.permissions)],
responses: {
200: {
description: "User followed",
content: {
"application/json": {
schema: Relationship.schema,
},
},
},
),
401: {
description: "Unauthorized",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
404: {
description: "User not found",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
},
request: {
params: schemas.param,
body: {
content: {
"application/json": {
schema: schemas.json,
},
},
},
},
});
export default apiRoute((app) =>
app.openapi(route, async (context) => {
const { id } = context.req.valid("param");
const { user } = context.get("auth");
const { reblogs, notify, languages } = context.req.valid("json");
if (!user) {
return context.json({ error: "Unauthorized" }, 401);
}
const otherUser = await User.fromId(id);
if (!otherUser) {
return context.json({ error: "User not found" }, 404);
}
let relationship = await Relationship.fromOwnerAndSubject(
user,
otherUser,
);
if (!relationship.data.following) {
relationship = await user.followRequest(otherUser, {
reblogs,
notify,
languages,
});
}
return context.json(relationship.toApi(), 200);
}),
);

View file

@ -1,16 +1,11 @@
import {
apiRoute,
applyConfig,
auth,
handleZodError,
idValidator,
} from "@/api";
import { zValidator } from "@hono/zod-validator";
import { apiRoute, applyConfig, auth, idValidator } from "@/api";
import { createRoute } from "@hono/zod-openapi";
import { and, gt, gte, lt, sql } from "drizzle-orm";
import { z } from "zod";
import { RolePermissions, Users } from "~/drizzle/schema";
import { Timeline } from "~/packages/database-interface/timeline";
import { User } from "~/packages/database-interface/user";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({
allowedMethods: ["GET"],
@ -43,44 +38,72 @@ export const schemas = {
}),
};
export default apiRoute((app) =>
app.on(
meta.allowedMethods,
meta.route,
zValidator("query", schemas.query, handleZodError),
zValidator("param", schemas.param, handleZodError),
auth(meta.auth, meta.permissions),
async (context) => {
const { id } = context.req.valid("param");
const { max_id, since_id, min_id, limit } =
context.req.valid("query");
const otherUser = await User.fromId(id);
// TODO: Add follower/following privacy settings
if (!otherUser) {
return context.json({ error: "User not found" }, 404);
}
const { objects, link } = await Timeline.getUserTimeline(
and(
max_id ? lt(Users.id, max_id) : undefined,
since_id ? gte(Users.id, since_id) : undefined,
min_id ? gt(Users.id, min_id) : undefined,
sql`EXISTS (SELECT 1 FROM "Relationships" WHERE "Relationships"."subjectId" = ${otherUser.id} AND "Relationships"."ownerId" = ${Users.id} AND "Relationships"."following" = true)`,
),
limit,
context.req.url,
);
return context.json(
await Promise.all(objects.map((object) => object.toApi())),
200,
{
Link: link,
const route = createRoute({
method: "get",
path: "/api/v1/accounts/{id}/followers",
summary: "Get account followers",
description:
"Gets an paginated list of accounts that follow the specified account",
middleware: [auth(meta.auth, meta.permissions)],
request: {
params: schemas.param,
query: schemas.query,
},
responses: {
200: {
description: "A list of accounts that follow the specified account",
content: {
"application/json": {
schema: z.array(User.schema),
},
);
},
headers: {
Link: {
description: "Links to the next and previous pages",
},
},
},
),
404: {
description: "The specified account was not found",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
},
});
export default apiRoute((app) =>
app.openapi(route, async (context) => {
const { id } = context.req.valid("param");
const { max_id, since_id, min_id, limit } = context.req.valid("query");
const otherUser = await User.fromId(id);
// TODO: Add follower/following privacy settings
if (!otherUser) {
return context.json({ error: "User not found" }, 404);
}
const { objects, link } = await Timeline.getUserTimeline(
and(
max_id ? lt(Users.id, max_id) : undefined,
since_id ? gte(Users.id, since_id) : undefined,
min_id ? gt(Users.id, min_id) : undefined,
sql`EXISTS (SELECT 1 FROM "Relationships" WHERE "Relationships"."subjectId" = ${otherUser.id} AND "Relationships"."ownerId" = ${Users.id} AND "Relationships"."following" = true)`,
),
limit,
context.req.url,
);
return context.json(
await Promise.all(objects.map((object) => object.toApi())),
200,
{
Link: link,
},
);
}),
);

View file

@ -1,16 +1,11 @@
import {
apiRoute,
applyConfig,
auth,
handleZodError,
idValidator,
} from "@/api";
import { zValidator } from "@hono/zod-validator";
import { apiRoute, applyConfig, auth, idValidator } from "@/api";
import { createRoute } from "@hono/zod-openapi";
import { and, gt, gte, lt, sql } from "drizzle-orm";
import { z } from "zod";
import { RolePermissions, Users } from "~/drizzle/schema";
import { Timeline } from "~/packages/database-interface/timeline";
import { User } from "~/packages/database-interface/user";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({
allowedMethods: ["GET"],
@ -43,43 +38,73 @@ export const schemas = {
}),
};
export default apiRoute((app) =>
app.on(
meta.allowedMethods,
meta.route,
zValidator("query", schemas.query, handleZodError),
zValidator("param", schemas.param, handleZodError),
auth(meta.auth, meta.permissions),
async (context) => {
const { id } = context.req.valid("param");
const { max_id, since_id, min_id } = context.req.valid("query");
const otherUser = await User.fromId(id);
if (!otherUser) {
return context.json({ error: "User not found" }, 404);
}
// TODO: Add follower/following privacy settings
const { objects, link } = await Timeline.getUserTimeline(
and(
max_id ? lt(Users.id, max_id) : undefined,
since_id ? gte(Users.id, since_id) : undefined,
min_id ? gt(Users.id, min_id) : undefined,
sql`EXISTS (SELECT 1 FROM "Relationships" WHERE "Relationships"."subjectId" = ${Users.id} AND "Relationships"."ownerId" = ${otherUser.id} AND "Relationships"."following" = true)`,
),
context.req.valid("query").limit,
context.req.url,
);
return context.json(
await Promise.all(objects.map((object) => object.toApi())),
200,
{
Link: link,
const route = createRoute({
method: "get",
path: "/api/v1/accounts/{id}/following",
summary: "Get account following",
description:
"Gets an paginated list of accounts that the specified account follows",
middleware: [auth(meta.auth, meta.permissions)],
request: {
params: schemas.param,
query: schemas.query,
},
responses: {
200: {
description:
"A list of accounts that the specified account follows",
content: {
"application/json": {
schema: z.array(User.schema),
},
);
},
headers: {
Link: {
description: "Link to the next page of results",
},
},
},
),
404: {
description: "User not found",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
},
});
export default apiRoute((app) =>
app.openapi(route, async (context) => {
const { id } = context.req.valid("param");
const { max_id, since_id, min_id } = context.req.valid("query");
const otherUser = await User.fromId(id);
if (!otherUser) {
return context.json({ error: "User not found" }, 404);
}
// TODO: Add follower/following privacy settings
const { objects, link } = await Timeline.getUserTimeline(
and(
max_id ? lt(Users.id, max_id) : undefined,
since_id ? gte(Users.id, since_id) : undefined,
min_id ? gt(Users.id, min_id) : undefined,
sql`EXISTS (SELECT 1 FROM "Relationships" WHERE "Relationships"."subjectId" = ${Users.id} AND "Relationships"."ownerId" = ${otherUser.id} AND "Relationships"."following" = true)`,
),
context.req.valid("query").limit,
context.req.url,
);
return context.json(
await Promise.all(objects.map((object) => object.toApi())),
200,
{
Link: link,
},
);
}),
);