diff --git a/package-lock.json b/package-lock.json index b65d85318..cdd18899b 100644 Binary files a/package-lock.json and b/package-lock.json differ diff --git a/package.json b/package.json index 444ff0d65..abb4576b5 100644 --- a/package.json +++ b/package.json @@ -118,7 +118,7 @@ "prom-client": "^15.1.3", "reflect-metadata": "^0.2.2", "tslib": "^2.8.1", - "typeorm": "^0.3.30", + "typeorm": "^1.0.0", "why-is-node-running": "^3.2.2", "wretch": "^3.0.8", "ws": "^8.21.0" diff --git a/src/api/routes/channels/#channel_id/index.ts b/src/api/routes/channels/#channel_id/index.ts index 236ddcf64..1c8572612 100644 --- a/src/api/routes/channels/#channel_id/index.ts +++ b/src/api/routes/channels/#channel_id/index.ts @@ -151,7 +151,7 @@ router.patch( const { channel_id } = req.params as { [key: string]: string }; const channel = await Channel.findOneOrFail({ where: { id: channel_id }, - relations: ["available_tags"], + relations: { available_tags: true }, }); if (channel.isThread()) { @@ -182,7 +182,7 @@ router.patch( where: { id: channel.parent_id as string, }, - relations: ["available_tags"], + relations: { available_tags: true }, }); if (!parent.available_tags) throw new Error("shoot, internetal error"); const realTags = new Map(parent.available_tags.map((tag) => [tag.id, tag])); diff --git a/src/api/routes/channels/#channel_id/invites.ts b/src/api/routes/channels/#channel_id/invites.ts index 8afc7dc06..3df4a5a85 100644 --- a/src/api/routes/channels/#channel_id/invites.ts +++ b/src/api/routes/channels/#channel_id/invites.ts @@ -111,7 +111,7 @@ router.get( const invites = await Invite.find({ where: { guild_id, channel_id }, - relations: PublicInviteRelation, + relations: Object.fromEntries(PublicInviteRelation.map((i) => [i, true])), //TODO: cleanup }); res.status(200).send(invites); diff --git a/src/api/routes/channels/#channel_id/messages/#message_id/reactions.ts b/src/api/routes/channels/#channel_id/messages/#message_id/reactions.ts index c045bb54a..709b7c73a 100644 --- a/src/api/routes/channels/#channel_id/messages/#message_id/reactions.ts +++ b/src/api/routes/channels/#channel_id/messages/#message_id/reactions.ts @@ -161,7 +161,7 @@ router.get( where: { id: In(reaction.user_ids), }, - select: PublicUserProjection, + select: Object.fromEntries(PublicUserProjection.map((i) => [i, true])), //TODO: cleanup take: limit, }) ).map((user) => user.toPublicUser()); diff --git a/src/api/routes/channels/#channel_id/messages/#message_id/threads.ts b/src/api/routes/channels/#channel_id/messages/#message_id/threads.ts index bb48b4fbe..bf37dfe18 100644 --- a/src/api/routes/channels/#channel_id/messages/#message_id/threads.ts +++ b/src/api/routes/channels/#channel_id/messages/#message_id/threads.ts @@ -44,7 +44,7 @@ router.post( const body = req.body as MessageThreadCreationSchema; const message = await Message.findOneOrFail({ where: { id: message_id, channel_id }, - relations: ["guild"], + relations: { guild: true }, }); const channel = await Channel.findOneOrFail({ where: { id: channel_id }, diff --git a/src/api/routes/channels/#channel_id/recipients.ts b/src/api/routes/channels/#channel_id/recipients.ts index c72b3aa45..7e7c5d348 100644 --- a/src/api/routes/channels/#channel_id/recipients.ts +++ b/src/api/routes/channels/#channel_id/recipients.ts @@ -65,7 +65,7 @@ router.put( user: ( await User.findOneOrFail({ where: { id: user_id }, - select: PublicUserProjection, + select: Object.fromEntries(PublicUserProjection.map((i) => [i, true])), //TODO: cleanup }) ).toPublicUser(), }, diff --git a/src/api/routes/channels/#channel_id/tags.ts b/src/api/routes/channels/#channel_id/tags.ts index ffdb428d6..73ca8a9d1 100644 --- a/src/api/routes/channels/#channel_id/tags.ts +++ b/src/api/routes/channels/#channel_id/tags.ts @@ -43,7 +43,7 @@ router.post( const channel = await Channel.findOneOrFail({ where: { id: channel_id }, - relations: ["available_tags"], + relations: { available_tags: true }, }); if (!channel.isForum()) throw new Error("is not thread only channel"); @@ -88,7 +88,7 @@ router.put( const channel = await Channel.findOneOrFail({ where: { id: channel_id }, - relations: ["available_tags"], + relations: { available_tags: true }, }); if (!channel.isForum()) throw new Error("is not thread only channel"); @@ -127,7 +127,7 @@ router.delete( const channel = await Channel.findOneOrFail({ where: { id: channel_id }, - relations: ["available_tags"], + relations: { available_tags: true }, }); if (!channel.isForum()) throw new Error("is not thread only channel"); diff --git a/src/api/routes/channels/#channel_id/threads.ts b/src/api/routes/channels/#channel_id/threads.ts index 9485ab752..b9850635b 100644 --- a/src/api/routes/channels/#channel_id/threads.ts +++ b/src/api/routes/channels/#channel_id/threads.ts @@ -56,7 +56,7 @@ router.post( const channel = await Channel.findOneOrFail({ where: { id: channel_id }, - relations: ["available_tags"], + relations: { available_tags: true }, }); if (!body.applied_tags?.length) { const required = channel.flags & Number(ChannelFlags.FLAGS.REQUIRE_TAG); diff --git a/src/api/routes/guilds/#guild_id/invites.ts b/src/api/routes/guilds/#guild_id/invites.ts index 7fb330deb..aadd040a8 100644 --- a/src/api/routes/guilds/#guild_id/invites.ts +++ b/src/api/routes/guilds/#guild_id/invites.ts @@ -37,7 +37,7 @@ router.get( const invites = await Invite.find({ where: { guild_id }, - relations: PublicInviteRelation, + relations: Object.fromEntries(PublicInviteRelation.map((i) => [i, true])), // TODO cleanup }); await Promise.all( diff --git a/src/api/routes/guilds/#guild_id/members/index.ts b/src/api/routes/guilds/#guild_id/members/index.ts index 2586d84d5..942da645d 100644 --- a/src/api/routes/guilds/#guild_id/members/index.ts +++ b/src/api/routes/guilds/#guild_id/members/index.ts @@ -60,7 +60,7 @@ router.get( const members = await Member.find({ where: { guild_id, ...query }, - select: PublicMemberProjection, + select: Object.fromEntries(PublicMemberProjection.map((i) => [i, true])), // TODO: cleanup take: limit, order: { id: "ASC" }, }); diff --git a/src/api/routes/guilds/#guild_id/messages/search.ts b/src/api/routes/guilds/#guild_id/messages/search.ts index 6d9bbe5e8..c3c4b3405 100644 --- a/src/api/routes/guilds/#guild_id/messages/search.ts +++ b/src/api/routes/guilds/#guild_id/messages/search.ts @@ -128,7 +128,7 @@ router.get( mentions = mentions instanceof Array ? mentions : mentions ? [mentions] : []; let roleids = [] as string[]; if (mentions) { - const ms = await Member.find({ where: { id: In(mentions), guild_id: req.params.guild_id as string }, relations: ["roles"] }); + const ms = await Member.find({ where: { id: In(mentions), guild_id: req.params.guild_id as string }, relations: { roles: true } }); const rSet = new Set(); ms.forEach((memb) => { memb.roles.forEach(({ id }) => rSet.add(id)); diff --git a/src/api/routes/guilds/#guild_id/templates.ts b/src/api/routes/guilds/#guild_id/templates.ts index f2782bb85..b6d513735 100644 --- a/src/api/routes/guilds/#guild_id/templates.ts +++ b/src/api/routes/guilds/#guild_id/templates.ts @@ -86,7 +86,7 @@ router.post( const { guild_id } = req.params as { [key: string]: string }; const guild = await Guild.findOneOrFail({ where: { id: guild_id }, - select: TemplateGuildProjection, + select: Object.fromEntries(TemplateGuildProjection.map((i) => [i, true])), //TODO: cleanup relations: { roles: true, channels: true }, }); const exists = await Template.findOne({ @@ -142,7 +142,7 @@ router.put( const { code, guild_id } = req.params as { [key: string]: string }; const guild = await Guild.findOneOrFail({ where: { id: guild_id }, - select: TemplateGuildProjection, + select: Object.fromEntries(TemplateGuildProjection.map((i) => [i, true])), //TODO: cleanup }); const template = await Template.create({ diff --git a/src/api/routes/invites/index.ts b/src/api/routes/invites/index.ts index a1ba3131a..323511ceb 100644 --- a/src/api/routes/invites/index.ts +++ b/src/api/routes/invites/index.ts @@ -42,7 +42,7 @@ router.get( const invite = await Invite.findOneOrFail({ where: { code: invite_code }, - relations: PublicInviteRelation, + relations: Object.fromEntries(PublicInviteRelation.map((i) => [i, true])), //TODO: clean up }); res.status(200).send(invite.toPublicJSON()); diff --git a/src/api/routes/users/#user_id/delete.ts b/src/api/routes/users/#user_id/delete.ts index 9b12c7bbb..cf3fb2541 100644 --- a/src/api/routes/users/#user_id/delete.ts +++ b/src/api/routes/users/#user_id/delete.ts @@ -46,7 +46,7 @@ router.post( const body = req.body as InstanceUserDeleteSchema | undefined; const user = await User.findOneOrFail({ where: { id: req.params.user_id as string }, - select: [...PrivateUserProjection, "data"], + select: Object.fromEntries([...PrivateUserProjection, "data"].map((i) => [i, true])), // TODO: clean up }); if ((body?.persistInstanceBan ?? true) && !(await InstanceBan.findOne({ where: { user_id: user.id } }))) diff --git a/src/api/routes/users/#user_id/profile.ts b/src/api/routes/users/#user_id/profile.ts index 84082afa3..bbe8faed7 100644 --- a/src/api/routes/users/#user_id/profile.ts +++ b/src/api/routes/users/#user_id/profile.ts @@ -120,7 +120,10 @@ router.get("/", route({ responses: { 200: { body: "UserProfileResponse" } } }), const relationshipsIntersection = relationshipsSelf.filter((r1) => relationshipsUser.some((r2) => r2.to_id === r1.to_id)); if (with_mutual_friends_count) mutual_friends_count = relationshipsIntersection.length; if (with_mutual_friends) { - const users = await User.find({ where: { id: In(relationshipsIntersection.map((r) => r.to_id)) }, select: PublicUserProjection }); + const users = await User.find({ + where: { id: In(relationshipsIntersection.map((r) => r.to_id)) }, + select: Object.fromEntries(PublicUserProjection.map((i) => [i, true])), + }); //TODO: clean up mutual_friends = users.map((u) => u.toPublicUser()); } } @@ -169,7 +172,7 @@ router.patch("/", route({ requestBody: "UserProfileModifySchema" }), async (req: if (body.banner) body.banner = await handleFile(`/banners/${req.user_id}`, body.banner as string); const user = await User.findOneOrFail({ where: { id: req.user_id }, - select: [...PrivateUserProjection, "data"], + select: Object.fromEntries([...PrivateUserProjection, "data"].map((i) => [i, true])), //TODO: cleanup }); if (body.bio) { diff --git a/src/api/routes/users/@me/index.ts b/src/api/routes/users/@me/index.ts index 1fda0afb6..ecb0b35cb 100644 --- a/src/api/routes/users/@me/index.ts +++ b/src/api/routes/users/@me/index.ts @@ -37,7 +37,7 @@ router.get( async (req: Request, res: Response) => { res.json( await User.findOne({ - select: PrivateUserProjection, + select: Object.fromEntries(PrivateUserProjection.map((i) => [i, true])), //TODO: cleanup where: { id: req.user_id }, }), ); @@ -65,7 +65,7 @@ router.patch( const user = await User.findOneOrFail({ where: { id: req.user_id }, - select: [...PrivateUserProjection, "data"], + select: Object.fromEntries([...PrivateUserProjection, "data"].map((i) => [i, true])), //TODO: cleanup }); // Populated on password change diff --git a/src/api/routes/users/@me/relationships.ts b/src/api/routes/users/@me/relationships.ts index d4d1eb1e8..6a6db5e4e 100644 --- a/src/api/routes/users/@me/relationships.ts +++ b/src/api/routes/users/@me/relationships.ts @@ -72,7 +72,7 @@ router.put( await User.findOneOrFail({ where: { id: req.params.user_id as string }, relations: { relationships: { to: true } }, - select: userProjection, + select: Object.fromEntries(userProjection.map((i) => [i, true])), // TODO: cleanup }), req.body.type ?? RelationshipType.friends, ), @@ -136,7 +136,7 @@ router.post( res, await User.findOneOrFail({ relations: { relationships: { to: true } }, - select: userProjection, + select: Object.fromEntries(userProjection.map((i) => [i, true])), // TODO: cleanup where: { discriminator: String(req.body.discriminator).padStart(4, "0"), //Discord send the discriminator as integer, we need to add leading zeroes username: req.body.username, @@ -165,12 +165,12 @@ router.delete( const user = await User.findOneOrFail({ where: { id: req.user_id }, - select: userProjection, + select: Object.fromEntries(userProjection.map((i) => [i, true])), // TODO: cleanup relations: { relationships: true }, }); const friend = await User.findOneOrFail({ where: { id: user_id }, - select: userProjection, + select: Object.fromEntries(userProjection.map((i) => [i, true])), // TODO: cleanup relations: { relationships: true }, }); @@ -215,8 +215,6 @@ router.delete( }, ); -export default router; - async function updateRelationship(req: Request, res: Response, friend: User, type: RelationshipType) { const id = friend.id; if (id === req.user_id) throw new HTTPError("You can't add yourself as a friend"); @@ -224,7 +222,7 @@ async function updateRelationship(req: Request, res: Response, friend: User, typ const user = await User.findOneOrFail({ where: { id: req.user_id }, relations: { relationships: { to: true } }, - select: userProjection, + select: Object.fromEntries(userProjection.map((i) => [i, true])), //TODO: cleanup }); let relationship = user.relationships.find((x) => x.to_id === id); @@ -316,3 +314,5 @@ async function updateRelationship(req: Request, res: Response, friend: User, typ return res.sendStatus(204); } + +export default router; diff --git a/src/database/entities/Channel.ts b/src/database/entities/Channel.ts index 6771fc407..1543d20b0 100644 --- a/src/database/entities/Channel.ts +++ b/src/database/entities/Channel.ts @@ -546,7 +546,7 @@ export class Channel extends BaseClass { channel_id: channel.id, user: await User.findOneOrFail({ where: { id: user_id }, - select: PublicUserProjection, + select: Object.fromEntries(PublicUserProjection.map((i) => [i, true])), //TODO: cleanup }), }, channel_id: channel.id, diff --git a/src/database/entities/Member.ts b/src/database/entities/Member.ts index b5b0b3982..e8e186602 100644 --- a/src/database/entities/Member.ts +++ b/src/database/entities/Member.ts @@ -335,7 +335,7 @@ export class Member extends BaseClassWithoutId { where: { id: guild_id, }, - relations: PublicGuildRelations, + relations: Object.fromEntries(PublicGuildRelations.map((i) => [i, true])), //TODO: clean up relationLoadStrategy: "query", }); const channelPositionsGuild = await Guild.findOneOrFail({ diff --git a/src/database/entities/User.ts b/src/database/entities/User.ts index 5c2c4fe12..434f16303 100644 --- a/src/database/entities/User.ts +++ b/src/database/entities/User.ts @@ -235,7 +235,7 @@ export class User extends BaseClass { static async getPublicUser(user_id: string): Promise { const user = await User.findOneOrFail({ where: { id: user_id }, - select: PublicUserProjection, + select: Object.fromEntries(PublicUserProjection.map((i) => [i, true])), // TODO: clean up }); return user.toPublicUser(); } diff --git a/src/gateway/opcodes/Identify.ts b/src/gateway/opcodes/Identify.ts index 5da55b046..e1ce6e8c3 100644 --- a/src/gateway/opcodes/Identify.ts +++ b/src/gateway/opcodes/Identify.ts @@ -49,6 +49,7 @@ import { GuildOrUnavailable, Intents, OPCodes, + OrmUtils, PresenceUpdateEvent, ReadyEventData, ReadyGuildDTO, @@ -260,7 +261,7 @@ export async function onIdentify(this: WebSocket, data: Payload) { where: { id: this.user_id }, select: { // We only want some member props - ...Object.fromEntries(["index", ...MemberPrivateProjection].map((x) => [x, true])), + ...OrmUtils.keysToObject(["index", ...(MemberPrivateProjection)]), settings: true, // guild settings roles: { id: true }, // the full role is fetched from the `guild` relation guild: { id: true }, @@ -353,7 +354,7 @@ export async function onIdentify(this: WebSocket, data: Payload) { type: Not(In([ChannelType.GUILD_PUBLIC_THREAD, ChannelType.GUILD_PRIVATE_THREAD, ChannelType.GUILD_NEWS_THREAD])), }, order: { guild_id: "ASC" }, - relations: ["available_tags"], + relations: { available_tags: true }, }), ), timePromise(() => diff --git a/src/util/dtos/DmChannelDTO.ts b/src/util/dtos/DmChannelDTO.ts index 9e6f05dad..ee541295a 100644 --- a/src/util/dtos/DmChannelDTO.ts +++ b/src/util/dtos/DmChannelDTO.ts @@ -46,7 +46,7 @@ export class DmChannelDTO { .map((r) => User.findOneOrFail({ where: { id: r.user_id }, - select: PublicUserProjection, + select: Object.fromEntries(PublicUserProjection.map((i) => [i, true])), // TODO: clean up }), ) || [], ) diff --git a/src/util/imports/OrmUtils.test.ts b/src/util/imports/OrmUtils.test.ts new file mode 100644 index 000000000..bc0ab055d --- /dev/null +++ b/src/util/imports/OrmUtils.test.ts @@ -0,0 +1,27 @@ +import moduleAlias from "module-alias"; +moduleAlias(__dirname + "../../../../package.json"); +import { config } from "dotenv"; +config({ quiet: true }); + +import { describe, test } from "node:test"; +import assert from "node:assert/strict"; +import { OrmUtils } from "./OrmUtils"; + +describe("OrmUtils", () => { + test("should be able convert string keys to boolean object", () => { + const keys = ["member", "member.user", "member.guild.meow", "member.guild", "guild.name"]; + const expected = { + member: { + user: true, + guild: { + meow: true, + }, + }, + guild: { + name: true, + }, + }; + const result = OrmUtils.keysToObject(keys); + assert.deepStrictEqual(result, expected); + }); +}); diff --git a/src/util/imports/OrmUtils.ts b/src/util/imports/OrmUtils.ts index 5a3c99e83..18dde56ea 100644 --- a/src/util/imports/OrmUtils.ts +++ b/src/util/imports/OrmUtils.ts @@ -2,6 +2,9 @@ // Copyright (c) 2015-2022 TypeORM. http://typeorm.github.io /* eslint-disable @typescript-eslint/no-explicit-any */ // @fc-license-skip +import { arrayGroupBy } from "@spacebar/extensions"; +import { FindOptionsSelect, FindOptionsWhere } from "typeorm"; + export class OrmUtils { // Checks if it's an object made by Object.create(null), {} or new Object() private static isPlainObject(item: unknown) { @@ -96,4 +99,23 @@ export class OrmUtils { return target; } + + // Copyright Spacebar & contributors 2026 (AGPLv3) + static keysToObject(keys: string[]) { + const target: FindOptionsSelect = {}; + for (const k of keys.filter((x) => !x.includes("."))) { + target[k] = true; + } + + const keyGroups = arrayGroupBy( + keys.filter((x) => x.includes(".")), + (k) => k.split(".", 2)[0], + ); + + for (const [k, v] of keyGroups) { + target[k] = this.keysToObject(v.map((vv) => vv.replace(k + ".", ""))); + } + + return target; + } } diff --git a/src/util/util/Permissions.ts b/src/util/util/Permissions.ts index 6585f6927..adb1ad5e5 100644 --- a/src/util/util/Permissions.ts +++ b/src/util/util/Permissions.ts @@ -7,6 +7,7 @@ import { BitField, BitFieldResolvable, BitFlag } from "./BitField"; import { HTTPError } from "lambert-server/HTTPError"; import { ChannelPermissionOverwrite, ChannelPermissionOverwriteType, ChannelType, UserFlags } from "@spacebar/schemas"; import { FindOneOptions } from "typeorm"; +import { OrmUtils } from "@spacebar/util"; export type PermissionResolvable = bigint | number | Permissions | PermissionResolvable[] | PermissionString; @@ -259,8 +260,8 @@ export async function getPermission( select: { id: true, flags: true }, }); const query = { - relations: ["recipients", "thread_members", "thread_members.member", ...(opts.channel_relations || [])], - select: ["type", "parent_id", "id", "recipients", "permission_overwrites", "owner_id", "guild_id", ...(opts.channel_select || [])], + relations: OrmUtils.keysToObject(["recipients", "thread_members", "thread_members.member", ...(opts.channel_relations || [])]), // TODO: cleanup + select: OrmUtils.keysToObject(["type", "parent_id", "id", "recipients", "permission_overwrites", "owner_id", "guild_id", ...(opts.channel_select || [])]), // TODO: cleanup } as FindOneOptions; if (typeof channel_id === "string") { channel = await Channel.findOneOrFail({ where: { id: channel_id }, ...query }); @@ -287,17 +288,17 @@ export async function getPermission( if (typeof guild_id === "string") { guild = await Guild.findOneOrFail({ where: { id: guild_id }, - select: ["id", "owner_id", ...(opts.guild_select || [])], - relations: opts.guild_relations, + select: !opts.guild_select ? { id: true, owner_id: true } : OrmUtils.keysToObject(["id", "owner_id", ...(opts.guild_select || [])]), // TODO: clean up + relations: !opts.guild_relations ? undefined : OrmUtils.keysToObject(opts.guild_relations), // TODO: clean up }); } else { guild = guild_id; } - if (guild.owner_id === user_id) return new Permissions(Permissions.FLAGS.ADMINISTRATOR); + if (guild!.owner_id === user_id) return new Permissions(Permissions.FLAGS.ADMINISTRATOR); member = await Member.findOneOrFail({ - where: { guild_id: guild.id, id: user_id }, - relations: ["roles", ...(opts.member_relations || [])], + where: { guild_id: guild!.id, id: user_id }, + relations: OrmUtils.keysToObject(["roles", ...(opts.member_relations || [])]), // TODO: clean up // select: [ // "id", // TODO: Bug in typeorm? adding these selects breaks the query. // "roles", diff --git a/src/util/util/Token.ts b/src/util/util/Token.ts index e8cd9ecb2..4eaa4bc9d 100644 --- a/src/util/util/Token.ts +++ b/src/util/util/Token.ts @@ -16,18 +16,17 @@ along with this program. If not, see . */ -import jwt from "jsonwebtoken"; -import { Config } from "./Config"; -import { InstanceBan, Session, User } from "../../database/entities"; import crypto from "node:crypto"; -import fs from "node:fs/promises"; import { existsSync } from "node:fs"; -// TODO: dont use deprecated APIs lol -import { FindOptionsRelationByString, FindOptionsSelectByString } from "typeorm"; -import { randomUpperString } from "@spacebar/api"; -import { TimeSpan } from "../../extensions/Timespan"; -import { HTTPError } from "lambert-server/HTTPError"; import path from "node:path"; +import fs from "node:fs/promises"; +import jwt from "jsonwebtoken"; +import { HTTPError } from "lambert-server/HTTPError"; +import { InstanceBan, Session, User } from "@spacebar/database"; +import { randomUpperString } from "@spacebar/api"; +import { TimeSpan } from "@spacebar/extensions"; +import { Config } from "./Config"; +import { OrmUtils } from "@spacebar/util"; /// Change history: /// 1 - Initial version with HS256 @@ -62,8 +61,8 @@ function rejectAndLog(rejectFunction: (reason?: unknown) => void, httpCode: numb export const checkToken = ( token: string, opts?: { - select?: FindOptionsSelectByString; - relations?: FindOptionsRelationByString; + select?: string[]; // TODO: clean up + relations?: string[]; // TODO: clean up ipAddress?: string; fingerprint?: string; }, @@ -85,8 +84,8 @@ export const checkToken = ( let [user, session] = await Promise.all([ User.findOne({ where: { id: decoded.id }, - select: [...(opts?.select || []), "id", "bot", "disabled", "deleted", "rights", "data"], - relations: opts?.relations, + select: OrmUtils.keysToObject([...(opts?.select || []), "id", "bot", "disabled", "deleted", "rights", "data"]), // TODO: clean up + relations: !opts?.relations ? undefined : OrmUtils.keysToObject(opts.relations), // TODO: clean up }), decoded.did ? Session.findOne({ where: { session_id: decoded.did, user_id: decoded.id } }) : undefined, ]);