193 lines
6.5 KiB
TypeScript
193 lines
6.5 KiB
TypeScript
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<Circle> {
|
|
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<Circle> {
|
|
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<Circle> {
|
|
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<void> {
|
|
// 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<void> {
|
|
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<Circle[]> = memo<(query: string) => Promise<Circle[]>, Circle[]>(this.#query);
|
|
async #query(query: string): Promise<Circle[]> {
|
|
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),
|
|
}),
|
|
);
|
|
}
|
|
}
|