refactor(database): ♻️ Move Token to its own ORM abstraction, optimize familiar_followers route

This commit is contained in:
Jesse Wierzbinski 2024-11-03 17:45:21 +01:00
parent 962c159ddd
commit 845041e4db
No known key found for this signature in database
55 changed files with 694 additions and 504 deletions

View file

@ -1,5 +1,5 @@
import type { Application as APIApplication } from "@versia/client/types";
import { db } from "@versia/kit/db";
import { Token, db } from "@versia/kit/db";
import { Applications } from "@versia/kit/tables";
import {
type InferInsertModel,
@ -81,15 +81,11 @@ export class Application extends BaseInterface<typeof Applications> {
public static async getFromToken(
token: string,
): Promise<Application | null> {
const result = await db.query.Tokens.findFirst({
where: (tokens, { eq }): SQL | undefined =>
eq(tokens.accessToken, token),
with: {
application: true,
},
});
const result = await Token.fromAccessToken(token);
return result?.application ? new Application(result.application) : null;
return result?.data.application
? new Application(result.data.application)
: null;
}
public static fromClientId(clientId: string): Promise<Application | null> {

171
classes/database/token.ts Normal file
View file

@ -0,0 +1,171 @@
import type { Token as ApiToken } from "@versia/client/types";
import { User, db } from "@versia/kit/db";
import { type Applications, Tokens } from "@versia/kit/tables";
import {
type InferInsertModel,
type InferSelectModel,
type SQL,
desc,
eq,
inArray,
} from "drizzle-orm";
import { z } from "zod";
import { BaseInterface } from "./base.ts";
export type TokenType = InferSelectModel<typeof Tokens> & {
application: InferSelectModel<typeof Applications> | null;
};
export class Token extends BaseInterface<typeof Tokens, TokenType> {
public static schema: z.ZodType<ApiToken> = z.object({
access_token: z.string(),
token_type: z.enum(["bearer"]),
scope: z.string(),
created_at: z.number(),
});
public async reload(): Promise<void> {
const reloaded = await Token.fromId(this.data.id);
if (!reloaded) {
throw new Error("Failed to reload token");
}
this.data = reloaded.data;
}
public static async fromId(id: string | null): Promise<Token | null> {
if (!id) {
return null;
}
return await Token.fromSql(eq(Tokens.id, id));
}
public static async fromIds(ids: string[]): Promise<Token[]> {
return await Token.manyFromSql(inArray(Tokens.id, ids));
}
public static async fromSql(
sql: SQL<unknown> | undefined,
orderBy: SQL<unknown> | undefined = desc(Tokens.id),
): Promise<Token | null> {
const found = await db.query.Tokens.findFirst({
where: sql,
orderBy,
with: {
application: true,
},
});
if (!found) {
return null;
}
return new Token(found);
}
public static async manyFromSql(
sql: SQL<unknown> | undefined,
orderBy: SQL<unknown> | undefined = desc(Tokens.id),
limit?: number,
offset?: number,
extra?: Parameters<typeof db.query.Tokens.findMany>[0],
): Promise<Token[]> {
const found = await db.query.Tokens.findMany({
where: sql,
orderBy,
limit,
offset,
with: {
application: true,
...extra?.with,
},
});
return found.map((s) => new Token(s));
}
public async update(newAttachment: Partial<TokenType>): Promise<TokenType> {
await db
.update(Tokens)
.set(newAttachment)
.where(eq(Tokens.id, this.id));
const updated = await Token.fromId(this.data.id);
if (!updated) {
throw new Error("Failed to update token");
}
this.data = updated.data;
return updated.data;
}
public save(): Promise<TokenType> {
return this.update(this.data);
}
public async delete(ids?: string[]): Promise<void> {
if (Array.isArray(ids)) {
await db.delete(Tokens).where(inArray(Tokens.id, ids));
} else {
await db.delete(Tokens).where(eq(Tokens.id, this.id));
}
}
public static async insert(
data: InferInsertModel<typeof Tokens>,
): Promise<Token> {
const inserted = (await db.insert(Tokens).values(data).returning())[0];
const token = await Token.fromId(inserted.id);
if (!token) {
throw new Error("Failed to insert token");
}
return token;
}
public static async insertMany(
data: InferInsertModel<typeof Tokens>[],
): Promise<Token[]> {
const inserted = await db.insert(Tokens).values(data).returning();
return await Token.fromIds(inserted.map((i) => i.id));
}
public get id(): string {
return this.data.id;
}
public static async fromAccessToken(
accessToken: string,
): Promise<Token | null> {
return await Token.fromSql(eq(Tokens.accessToken, accessToken));
}
/**
* Retrieves the associated user from this token
*
* @returns The user associated with this token
*/
public async getUser(): Promise<User | null> {
if (!this.data.userId) {
return null;
}
return await User.fromId(this.data.userId);
}
public toApi(): ApiToken {
return {
access_token: this.data.accessToken,
token_type: "Bearer",
scope: this.data.scope,
created_at: Math.floor(
new Date(this.data.createdAt).getTime() / 1000,
),
};
}
}

View file

@ -1,11 +0,0 @@
import type { Tokens } from "@versia/kit/tables";
import type { InferSelectModel } from "drizzle-orm";
/**
* The type of token.
*/
export enum TokenType {
Bearer = "Bearer",
}
export type Token = InferSelectModel<typeof Tokens>;

View file

@ -3,18 +3,10 @@ import type {
FollowAccept,
FollowReject,
} from "@versia/federation/types";
import { User, db } from "@versia/kit/db";
import {
Applications,
type Instances,
type Roles,
Tokens,
type Users,
} from "@versia/kit/tables";
import { type InferSelectModel, type SQL, eq, sql } from "drizzle-orm";
import type { ApplicationType } from "~/classes/database/application.ts";
import { type Application, type Token, type User, db } from "@versia/kit/db";
import type { Instances, Roles, Users } from "@versia/kit/tables";
import { type InferSelectModel, type SQL, sql } from "drizzle-orm";
import type { EmojiWithInstance } from "~/classes/database/emoji.ts";
import type { Token } from "./token.ts";
export type UserType = InferSelectModel<typeof Users>;
@ -31,23 +23,7 @@ export type UserWithRelations = UserType & {
roles: InferSelectModel<typeof Roles>[];
};
export const userRelations: {
instance: true;
emojis: {
with: {
emoji: {
with: {
instance: true;
};
};
};
};
roles: {
with: {
role: true;
};
};
} = {
export const userRelations = {
instance: true,
emojis: {
with: {
@ -63,7 +39,7 @@ export const userRelations: {
role: true,
},
},
};
} as const;
export const userExtras = {
followerCount:
@ -103,19 +79,10 @@ export const userExtrasTemplate = (
export interface AuthData {
user: User | null;
token: string;
application: ApplicationType | null;
token: Token | null;
application: Application | null;
}
export const getFromHeader = async (value: string): Promise<AuthData> => {
const token = value.split(" ")[1];
const { user, application } =
await retrieveUserAndApplicationFromToken(token);
return { user, token, application };
};
export const transformOutputToUserWithRelations = (
user: Omit<UserType, "endpoints"> & {
followerCount: unknown;
@ -180,52 +147,6 @@ export const findManyUsers = async (
return output.map((user) => transformOutputToUserWithRelations(user));
};
export const retrieveUserAndApplicationFromToken = async (
accessToken: string,
): Promise<{
user: User | null;
application: ApplicationType | null;
}> => {
if (!accessToken) {
return { user: null, application: null };
}
const output = (
await db
.select({
token: Tokens,
application: Applications,
})
.from(Tokens)
.leftJoin(Applications, eq(Tokens.applicationId, Applications.id))
.where(eq(Tokens.accessToken, accessToken))
.limit(1)
)[0];
if (!output?.token.userId) {
return { user: null, application: null };
}
const user = await User.fromId(output.token.userId);
return { user, application: output.application ?? null };
};
export const retrieveToken = async (
accessToken: string,
): Promise<Token | null> => {
if (!accessToken) {
return null;
}
return (
(await db.query.Tokens.findFirst({
where: (tokens, { eq }): SQL | undefined =>
eq(tokens.accessToken, accessToken),
})) ?? null
);
};
export const followRequestToVersia = (
follower: User,
followee: User,