import { Claims } from './claims'; import { sql } from 'bun'; import { first } from 'lodash'; import { BadRequestError, NotFoundError, UnauthorizedError } from '../utilities/errors'; import { CreateCircleRequest, UpdateCircleRequest } from '../utilities/requestModels'; import { memo } from '../utilities/helpers'; import { CircleId, PlayerId, UserId } from '../utilities/secureIds'; import { orm } from './orm'; import { User } from './user'; export class Circle { id: CircleId; owningUserId: UserId; name: string; isPublic: boolean; imagePath?: string; colour?: string; constructor({ id, owningUserId, name, isPublic, imagePath, colour, }: { id: CircleId; owningUserId: UserId; name: string; isPublic: boolean; imagePath?: string; colour?: string; }) { this.id = id; this.owningUserId = owningUserId; this.name = name; this.isPublic = isPublic; this.imagePath = imagePath; this.colour = colour; } } export class CircleOrm { async create(model: CreateCircleRequest, claims?: Claims): Promise { if (model.isPublic && claims && !claims.test(Claims.ADMIN, Claims.CIRCLES.PUBLIC.CREATE)) { throw new UnauthorizedError(); } await sql`INSERT INTO circles (owning_user_id, name, is_public, colour) VALUES (${claims?.userId.raw}, ${model.name}, ${model.isPublic}, ${model.colour})`; const newRecordId: string = (first(await sql`SELECT lastval();`) as any)?.lastval as string; return await this.get(CircleId.fromID(newRecordId)); } async get(id: CircleId, claims?: Claims): Promise { const record: any = first( await sql`SELECT * FROM circles WHERE id = ${id.raw} LIMIT 1`, ); if (!record) { throw new NotFoundError('No matching game exists'); } let user: User; if ( claims && !claims.test(Claims.ADMIN) && !(claims.test(Claims.CIRCLES.PUBLIC.READ) && record.is_public) && !(claims.test(Claims.CIRCLES.OWNED.READ) && record.owning_user_id === claims.userId.raw) && !( claims.test(Claims.CIRCLES.PRIVATE.READ_IF_MEMBER) && (user = await orm.users.get(claims.userId)) && (await sql`SELECT * FROM player_circles WHERE circle_id = ${id.raw}`).some( (x: { player_id: string }) => x.player_id === user.playerId.raw, ) ) ) { throw new UnauthorizedError(); } return new Circle({ id: CircleId.fromID(record.id), owningUserId: UserId.fromID(record.owning_user_id), name: record.name, isPublic: record.is_public, imagePath: record.image_path, colour: record.colour, }); } async update(id: CircleId, patch: UpdateCircleRequest): Promise { const circle = await this.get(id); circle.name = patch.name ?? circle.name; circle.colour = patch.colour ?? circle.colour; circle.imagePath = patch.imagePath ?? circle.imagePath; await sql`UPDATE circles SET name=${circle.name}, colour=${circle.colour}, image_path=${circle.imagePath} WHERE id = ${id.raw}`; return await this.get(id); } async drop(id: CircleId): Promise { // Ensure record exists before attempting to delete await this.get(id); await sql.transaction(async (tx) => { await tx`DELETE FROM player_circles WHERE circle_id = ${id.raw}`; await tx`DELETE FROM circle_invites WHERE circle_id = ${id.raw}`; await tx`DELETE FROM circle_comments WHERE circle_id = ${id.raw}`; await tx`DELETE FROM circles WHERE id = ${id.raw}`; }); return; } async invite( circleId: CircleId, relatedRecord: PlayerId | UserId | string | undefined, claims: Claims, ): Promise { if (relatedRecord === undefined) { throw new BadRequestError(); } const circle = await this.get(circleId, claims); if ( claims && ((circle.isPublic && !claims.test(Claims.CIRCLES.PUBLIC.USERS.INVITE)) || (!circle.isPublic && !(claims.test(Claims.CIRCLES.OWNED.USERS.INVITE) && circle.owningUserId === claims.userId))) ) { throw new UnauthorizedError(); } let invitedUserId: UserId | undefined; if (relatedRecord instanceof UserId) { invitedUserId = relatedRecord; } else if (relatedRecord instanceof PlayerId) { invitedUserId = (await orm.users.getByPlayer(relatedRecord))?.id; } else { invitedUserId = (await orm.users.getByEmail(relatedRecord))?.id; } if (!invitedUserId) { throw new BadRequestError(); } await sql`INSERT INTO circle_invites(invited_user_id, invited_by_user_id, circle_id) VALUES (${invitedUserId.raw}, ${claims.userId.raw}, ${circleId.raw})`; } query: (query: string) => Promise = memo<(query: string) => Promise, Circle[]>(this.#query); async #query(query: string): Promise { const queryResult: any = await sql` SELECT id, name, owning_user_id, is_public FROM (SELECT *, SIMILARITY(${query}, name) as similarity FROM circles WHERE is_public=true) WHERE similarity > 0 ORDER BY similarity LIMIT 5;`; if (!queryResult) { throw new NotFoundError('No matching circles exists'); } return queryResult.map( (x: { id: string; name: string; owning_user_id: string; is_public: boolean }) => new Circle({ id: CircleId.fromID(x.id), name: x.name, isPublic: x.is_public, owningUserId: UserId.fromID(x.owning_user_id), }), ); } }