sendou.ink/app/features/sendouq/QRepository.server.ts
Kalle 7dec8c572e
SendouQ Season 2 changes (#1542)
* Initial

* Saves preferences

* Include TW

* mapModePreferencesToModeList

* mapPoolFromPreferences initial

* Preference to map pool

* Adjust seed

* q.looking tests

* adds about created map preferences to memento in the correct spot (two preferrers)

* Failing test about modes

* Mode preferences to memento

* Remove old Plus Voting code

* Fix seeding

* find match by id via kysely

* View map memento

* Fix up map list generation logic

* Mode memento info

* Future match modes

* Add TODO

* Migration number

* Migrate test DB

* Remove old map pool code

* createGroupFromPrevious new

* Settings styling

* VC to settings

* Weapon pool

* Add TODOs

* Progress

* Adjust mode exclusion policy

* Progress

* Progress

* Progress

* Notes in progress

* Note feedback after submit

* Textarea styling

* Unskip tests

* Note sorting failing test

* Private note in Q

* Ownerpicksmaps later

* New bottom section

* Mobile layout initial

* Add basic match meta

* Tabs initial

* Sticky tab

* Unseen messages in match page

* Front page i18n

* Settings i18n

* Looking 18n

* Chat i18n

* Progress

* Tranfer weapon pools script

* Sticky on match page

* Match page translations

* i18n - tiers page

* Preparing page i18n

* Icon

* Show add note right after report
2023-11-30 20:57:06 +02:00

252 lines
7.1 KiB
TypeScript

import { sql } from "kysely";
import { jsonArrayFrom, jsonObjectFrom } from "kysely/helpers/sqlite";
import { db } from "~/db/sql";
import type {
Tables,
TablesInsertable,
UserMapModePreferences,
} from "~/db/tables";
import { COMMON_USER_FIELDS } from "~/utils/kysely.server";
import type { LookingGroupWithInviteCode } from "./q-types";
import { nanoid } from "nanoid";
import { INVITE_CODE_LENGTH } from "~/constants";
import { dateToDatabaseTimestamp } from "~/utils/dates";
export function mapModePreferencesByGroupId(groupId: number) {
return db
.selectFrom("GroupMember")
.innerJoin("User", "User.id", "GroupMember.userId")
.select(["User.id as userId", "User.mapModePreferences as preferences"])
.where("GroupMember.groupId", "=", groupId)
.where("User.mapModePreferences", "is not", null)
.execute() as Promise<
{ userId: number; preferences: UserMapModePreferences }[]
>;
}
// groups visible for longer to make development easier
const SECONDS_TILL_STALE =
process.env.NODE_ENV === "development" ? 1_000_000 : 1_800;
export async function findLookingGroups({
minGroupSize,
maxGroupSize,
ownGroupId,
includeChatCode = false,
includeMapModePreferences = false,
loggedInUserId,
}: {
minGroupSize?: number;
maxGroupSize?: number;
ownGroupId: number;
includeChatCode?: boolean;
includeMapModePreferences?: boolean;
loggedInUserId?: number;
}): Promise<LookingGroupWithInviteCode[]> {
const rows = await db
.selectFrom("Group")
.leftJoin("GroupMatch", (join) =>
join.on((eb) =>
eb.or([
eb("GroupMatch.alphaGroupId", "=", eb.ref("Group.id")),
eb("GroupMatch.bravoGroupId", "=", eb.ref("Group.id")),
]),
),
)
.select((eb) => [
"Group.id",
"Group.createdAt",
"Group.chatCode",
"Group.inviteCode",
jsonArrayFrom(
eb
.selectFrom("GroupMember")
.innerJoin("User", "User.id", "GroupMember.userId")
.leftJoin("PlusTier", "PlusTier.userId", "GroupMember.userId")
.select((arrayEb) => [
...COMMON_USER_FIELDS,
"User.qWeaponPool as weapons",
"PlusTier.tier as plusTier",
"GroupMember.note",
"User.languages",
"User.vc",
jsonObjectFrom(
eb
.selectFrom("PrivateUserNote")
.select([
"PrivateUserNote.sentiment",
"PrivateUserNote.text",
"PrivateUserNote.updatedAt",
])
.where("authorId", "=", loggedInUserId ?? -1)
.where("targetId", "=", arrayEb.ref("User.id")),
).as("privateNote"),
sql<
string | null
>`IIF(COALESCE("User"."patronTier", 0) >= 2, "User"."css" ->> 'chat', null)`.as(
"chatNameColor",
),
])
.where("GroupMember.groupId", "=", eb.ref("Group.id"))
.groupBy("GroupMember.userId"),
).as("members"),
])
.$if(includeMapModePreferences, (qb) =>
qb.select((eb) =>
jsonArrayFrom(
eb
.selectFrom("GroupMember")
.innerJoin("User", "User.id", "GroupMember.userId")
.select("User.mapModePreferences")
.where("GroupMember.groupId", "=", eb.ref("Group.id"))
.where("User.mapModePreferences", "is not", null),
).as("mapModePreferences"),
),
)
.where("Group.status", "=", "ACTIVE")
.where("GroupMatch.id", "is", null)
.where((eb) =>
eb.or([
eb(
"Group.latestActionAt",
">",
sql`(unixepoch() - ${SECONDS_TILL_STALE})`,
),
eb("Group.id", "=", ownGroupId),
]),
)
.execute();
// TODO: a bit weird we filter chatCode here but not inviteCode and do some logic about filtering
return rows
.map((row) => {
return {
...row,
chatCode: includeChatCode ? row.chatCode : undefined,
mapModePreferences: row.mapModePreferences?.map(
(c) => c.mapModePreferences,
) as NonNullable<Tables["User"]["mapModePreferences"]>[],
members: row.members.map((member) => {
return {
...member,
languages: member.languages ? member.languages.split(",") : [],
} as LookingGroupWithInviteCode["members"][number];
}),
};
})
.filter((group) => {
if (group.id === ownGroupId) return true;
if (maxGroupSize && group.members.length > maxGroupSize) return false;
if (minGroupSize && group.members.length < minGroupSize) return false;
return true;
});
}
type CreateGroupArgs = {
status: Exclude<Tables["Group"]["status"], "INACTIVE">;
userId: number;
};
export function createGroup(args: CreateGroupArgs) {
return db.transaction().execute(async (trx) => {
const createdGroup = await trx
.insertInto("Group")
.values({
inviteCode: nanoid(INVITE_CODE_LENGTH),
chatCode: nanoid(INVITE_CODE_LENGTH),
status: args.status,
})
.returning("id")
.executeTakeFirstOrThrow();
await trx
.insertInto("GroupMember")
.values({
groupId: createdGroup.id,
userId: args.userId,
role: "OWNER",
})
.execute();
return createdGroup;
});
}
type CreateGroupFromPreviousGroupArgs = {
previousGroupId: number;
members: {
id: number;
role: Tables["GroupMember"]["role"];
}[];
};
export async function createGroupFromPrevious(
args: CreateGroupFromPreviousGroupArgs,
) {
return db.transaction().execute(async (trx) => {
const createdGroup = await trx
.insertInto("Group")
.columns(["teamId", "chatCode", "inviteCode", "status"])
.expression((eb) =>
eb
.selectFrom("Group")
.select((eb) => [
"Group.teamId",
"Group.chatCode",
eb.val(nanoid(INVITE_CODE_LENGTH)).as("inviteCode"),
eb.val("PREPARING").as("status"),
])
.where("Group.id", "=", args.previousGroupId),
)
.returning("id")
.executeTakeFirstOrThrow();
await trx
.insertInto("GroupMember")
.values(
args.members.map((member) => ({
groupId: createdGroup.id,
userId: member.id,
role: member.role,
})),
)
.execute();
return createdGroup;
});
}
export function upsertPrivateUserNote(
args: TablesInsertable["PrivateUserNote"],
) {
return db
.insertInto("PrivateUserNote")
.values({
authorId: args.authorId,
targetId: args.targetId,
sentiment: args.sentiment,
text: args.text,
})
.onConflict((oc) =>
oc.columns(["authorId", "targetId"]).doUpdateSet({
sentiment: args.sentiment,
text: args.text,
updatedAt: dateToDatabaseTimestamp(new Date()),
}),
)
.execute();
}
export function deletePrivateUserNote({
authorId,
targetId,
}: {
authorId: number;
targetId: number;
}) {
return db
.deleteFrom("PrivateUserNote")
.where("authorId", "=", authorId)
.where("targetId", "=", targetId)
.execute();
}