diff --git a/web/hooks/use-contract.ts b/web/hooks/use-contract.ts index 9810d9d4..acaf7730 100644 --- a/web/hooks/use-contract.ts +++ b/web/hooks/use-contract.ts @@ -2,16 +2,16 @@ import { useEffect } from 'react' import { useFirestoreDocumentData } from '@react-query-firebase/firestore' import { Contract, - contractDocRef, + contracts, listenForContract, } from 'web/lib/firebase/contracts' import { useStateCheckEquality } from './use-state-check-equality' -import { DocumentData } from 'firebase/firestore' +import { doc, DocumentData } from 'firebase/firestore' export const useContract = (contractId: string) => { const result = useFirestoreDocumentData( ['contracts', contractId], - contractDocRef(contractId), + doc(contracts, contractId), { subscribe: true, includeMetadataChanges: true } ) diff --git a/web/hooks/use-user.ts b/web/hooks/use-user.ts index c4d1dff9..158235ca 100644 --- a/web/hooks/use-user.ts +++ b/web/hooks/use-user.ts @@ -2,7 +2,7 @@ import { useEffect, useState } from 'react' import { useFirestoreDocumentData } from '@react-query-firebase/firestore' import { QueryClient } from 'react-query' -import { DocumentData } from 'firebase/firestore' +import { doc, DocumentData } from 'firebase/firestore' import { PrivateUser } from 'common/user' import { getUser, @@ -10,7 +10,7 @@ import { listenForPrivateUser, listenForUser, User, - userDocRef, + users, } from 'web/lib/firebase/users' import { useStateCheckEquality } from './use-state-check-equality' import { identifyUser, setUserProperty } from 'web/lib/service/analytics' @@ -49,7 +49,7 @@ export const usePrivateUser = (userId?: string) => { export const useUserById = (userId: string) => { const result = useFirestoreDocumentData( ['users', userId], - userDocRef(userId), + doc(users, userId), { subscribe: true, includeMetadataChanges: true } ) diff --git a/web/lib/firebase/contracts.ts b/web/lib/firebase/contracts.ts index f177d841..d5fb85cb 100644 --- a/web/lib/firebase/contracts.ts +++ b/web/lib/firebase/contracts.ts @@ -1,6 +1,5 @@ import dayjs from 'dayjs' import { - getFirestore, doc, setDoc, deleteDoc, @@ -16,8 +15,7 @@ import { } from 'firebase/firestore' import { sortBy, sum } from 'lodash' -import { app } from './init' -import { getValues, listenForValue, listenForValues } from './utils' +import { coll, getValues, listenForValue, listenForValues } from './utils' import { BinaryContract, Contract } from 'common/contract' import { getDpmProbability } from 'common/calculate-dpm' import { createRNG, shuffle } from 'common/util/random' @@ -28,6 +26,9 @@ import { MAX_FEED_CONTRACTS } from 'common/recommended-contracts' import { Bet } from 'common/bet' import { Comment } from 'common/comment' import { ENV_CONFIG } from 'common/envs/constants' + +export const contracts = coll('contracts') + export type { Contract } export function contractPath(contract: Contract) { @@ -86,83 +87,72 @@ export function tradingAllowed(contract: Contract) { ) } -const db = getFirestore(app) -export const contractCollection = collection(db, 'contracts') -export const contractDocRef = (contractId: string) => - doc(db, 'contracts', contractId) - // Push contract to Firestore export async function setContract(contract: Contract) { - const docRef = doc(db, 'contracts', contract.id) - await setDoc(docRef, contract) + await setDoc(doc(contracts, contract.id), contract) } export async function updateContract( contractId: string, update: Partial ) { - const docRef = doc(db, 'contracts', contractId) - await updateDoc(docRef, update) + await updateDoc(doc(contracts, contractId), update) } export async function getContractFromId(contractId: string) { - const docRef = doc(db, 'contracts', contractId) - const result = await getDoc(docRef) - - return result.exists() ? (result.data() as Contract) : undefined + const result = await getDoc(doc(contracts, contractId)) + return result.exists() ? result.data() : undefined } export async function getContractFromSlug(slug: string) { - const q = query(contractCollection, where('slug', '==', slug)) + const q = query(contracts, where('slug', '==', slug)) const snapshot = await getDocs(q) - - return snapshot.empty ? undefined : (snapshot.docs[0].data() as Contract) + return snapshot.empty ? undefined : snapshot.docs[0].data() } export async function deleteContract(contractId: string) { - const docRef = doc(db, 'contracts', contractId) - await deleteDoc(docRef) + await deleteDoc(doc(contracts, contractId)) } export async function listContracts(creatorId: string): Promise { const q = query( - contractCollection, + contracts, where('creatorId', '==', creatorId), orderBy('createdTime', 'desc') ) const snapshot = await getDocs(q) - return snapshot.docs.map((doc) => doc.data() as Contract) + return snapshot.docs.map((doc) => doc.data()) } export async function listTaggedContractsCaseInsensitive( tag: string ): Promise { const q = query( - contractCollection, + contracts, where('lowercaseTags', 'array-contains', tag.toLowerCase()), orderBy('createdTime', 'desc') ) const snapshot = await getDocs(q) - return snapshot.docs.map((doc) => doc.data() as Contract) + return snapshot.docs.map((doc) => doc.data()) } export async function listAllContracts( n: number, before?: string ): Promise { - let q = query(contractCollection, orderBy('createdTime', 'desc'), limit(n)) + let q = query(contracts, orderBy('createdTime', 'desc'), limit(n)) if (before != null) { - const snap = await getDoc(doc(db, 'contracts', before)) + const snap = await getDoc(doc(contracts, before)) q = query(q, startAfter(snap)) } const snapshot = await getDocs(q) - return snapshot.docs.map((doc) => doc.data() as Contract) + return snapshot.docs.map((doc) => doc.data()) } export function listenForContracts( setContracts: (contracts: Contract[]) => void ) { - const q = query(contractCollection, orderBy('createdTime', 'desc')) + const q = query(contracts, orderBy('createdTime', 'desc')) return listenForValues(q, setContracts) } @@ -171,7 +161,7 @@ export function listenForUserContracts( setContracts: (contracts: Contract[]) => void ) { const q = query( - contractCollection, + contracts, where('creatorId', '==', creatorId), orderBy('createdTime', 'desc') ) @@ -179,7 +169,7 @@ export function listenForUserContracts( } const activeContractsQuery = query( - contractCollection, + contracts, where('isResolved', '==', false), where('visibility', '==', 'public'), where('volume7Days', '>', 0) @@ -196,7 +186,7 @@ export function listenForActiveContracts( } const inactiveContractsQuery = query( - contractCollection, + contracts, where('isResolved', '==', false), where('closeTime', '>', Date.now()), where('visibility', '==', 'public'), @@ -214,7 +204,7 @@ export function listenForInactiveContracts( } const newContractsQuery = query( - contractCollection, + contracts, where('isResolved', '==', false), where('volume7Days', '==', 0), where('createdTime', '>', Date.now() - 7 * DAY_MS) @@ -230,7 +220,7 @@ export function listenForContract( contractId: string, setContract: (contract: Contract | null) => void ) { - const contractRef = doc(contractCollection, contractId) + const contractRef = doc(contracts, contractId) return listenForValue(contractRef, setContract) } @@ -242,7 +232,7 @@ function chooseRandomSubset(contracts: Contract[], count: number) { } const hotContractsQuery = query( - contractCollection, + contracts, where('isResolved', '==', false), where('visibility', '==', 'public'), orderBy('volume24Hours', 'desc'), @@ -262,22 +252,22 @@ export function listenForHotContracts( } export async function getHotContracts() { - const contracts = await getValues(hotContractsQuery) + const data = await getValues(hotContractsQuery) return sortBy( - chooseRandomSubset(contracts, 10), + chooseRandomSubset(data, 10), (contract) => -1 * contract.volume24Hours ) } export async function getContractsBySlugs(slugs: string[]) { - const q = query(contractCollection, where('slug', 'in', slugs)) + const q = query(contracts, where('slug', 'in', slugs)) const snapshot = await getDocs(q) - const contracts = snapshot.docs.map((doc) => doc.data() as Contract) - return sortBy(contracts, (contract) => -1 * contract.volume24Hours) + const data = snapshot.docs.map((doc) => doc.data()) + return sortBy(data, (contract) => -1 * contract.volume24Hours) } const topWeeklyQuery = query( - contractCollection, + contracts, where('isResolved', '==', false), orderBy('volume7Days', 'desc'), limit(MAX_FEED_CONTRACTS) @@ -287,7 +277,7 @@ export async function getTopWeeklyContracts() { } const closingSoonQuery = query( - contractCollection, + contracts, where('isResolved', '==', false), where('visibility', '==', 'public'), where('closeTime', '>', Date.now()), @@ -296,15 +286,12 @@ const closingSoonQuery = query( ) export async function getClosingSoonContracts() { - const contracts = await getValues(closingSoonQuery) - return sortBy( - chooseRandomSubset(contracts, 2), - (contract) => contract.closeTime - ) + const data = await getValues(closingSoonQuery) + return sortBy(chooseRandomSubset(data, 2), (contract) => contract.closeTime) } export async function getRecentBetsAndComments(contract: Contract) { - const contractDoc = doc(db, 'contracts', contract.id) + const contractDoc = doc(contracts, contract.id) const [recentBets, recentComments] = await Promise.all([ getValues( diff --git a/web/lib/firebase/groups.ts b/web/lib/firebase/groups.ts index d7244f98..36b05452 100644 --- a/web/lib/firebase/groups.ts +++ b/web/lib/firebase/groups.ts @@ -1,7 +1,7 @@ import { - collection, deleteDoc, doc, + getDocs, query, updateDoc, where, @@ -9,11 +9,16 @@ import { import { sortBy } from 'lodash' import { Group } from 'common/group' import { getContractFromId } from './contracts' -import { db } from './init' -import { getValue, getValues, listenForValue, listenForValues } from './utils' +import { + coll, + getValue, + getValues, + listenForValue, + listenForValues, +} from './utils' import { filterDefined } from 'common/util/array' -const groupCollection = collection(db, 'groups') +export const groups = coll('groups') export function groupPath( groupSlug: string, @@ -23,30 +28,29 @@ export function groupPath( } export function updateGroup(group: Group, updates: Partial) { - return updateDoc(doc(groupCollection, group.id), updates) + return updateDoc(doc(groups, group.id), updates) } export function deleteGroup(group: Group) { - return deleteDoc(doc(groupCollection, group.id)) + return deleteDoc(doc(groups, group.id)) } export async function listAllGroups() { - return getValues(groupCollection) + return getValues(groups) } export function listenForGroups(setGroups: (groups: Group[]) => void) { - return listenForValues(groupCollection, setGroups) + return listenForValues(groups, setGroups) } export function getGroup(groupId: string) { - return getValue(doc(groupCollection, groupId)) + return getValue(doc(groups, groupId)) } export async function getGroupBySlug(slug: string) { - const q = query(groupCollection, where('slug', '==', slug)) - const groups = await getValues(q) - - return groups.length === 0 ? null : groups[0] + const q = query(groups, where('slug', '==', slug)) + const docs = (await getDocs(q)).docs + return docs.length === 0 ? null : docs[0].data() } export async function getGroupContracts(group: Group) { @@ -68,14 +72,14 @@ export function listenForGroup( groupId: string, setGroup: (group: Group | null) => void ) { - return listenForValue(doc(groupCollection, groupId), setGroup) + return listenForValue(doc(groups, groupId), setGroup) } export function listenForMemberGroups( userId: string, setGroups: (groups: Group[]) => void ) { - const q = query(groupCollection, where('memberIds', 'array-contains', userId)) + const q = query(groups, where('memberIds', 'array-contains', userId)) return listenForValues(q, (groups) => { const sorted = sortBy(groups, [(group) => -group.mostRecentActivityTime]) @@ -87,12 +91,8 @@ export async function getGroupsWithContractId( contractId: string, setGroups: (groups: Group[]) => void ) { - const q = query( - groupCollection, - where('contractIds', 'array-contains', contractId) - ) - const groups = await getValues(q) - setGroups(groups) + const q = query(groups, where('contractIds', 'array-contains', contractId)) + setGroups(await getValues(q)) } export async function joinGroup(group: Group, userId: string): Promise { diff --git a/web/lib/firebase/manalinks.ts b/web/lib/firebase/manalinks.ts index 67c7a00a..532534df 100644 --- a/web/lib/firebase/manalinks.ts +++ b/web/lib/firebase/manalinks.ts @@ -1,18 +1,12 @@ -import { - collection, - getDoc, - orderBy, - query, - setDoc, - where, -} from 'firebase/firestore' +import { getDoc, orderBy, query, setDoc, where } from 'firebase/firestore' import { doc } from 'firebase/firestore' import { Manalink } from '../../../common/manalink' -import { db } from './init' import { customAlphabet } from 'nanoid' -import { listenForValues } from './utils' +import { coll, listenForValues } from './utils' import { useEffect, useState } from 'react' +export const manalinks = coll('manalinks') + export async function createManalink(data: { fromId: string amount: number @@ -45,29 +39,25 @@ export async function createManalink(data: { message, } - const ref = doc(db, 'manalinks', slug) - await setDoc(ref, manalink) + await setDoc(doc(manalinks, slug), manalink) return slug } -const manalinkCol = collection(db, 'manalinks') - // TODO: This required an index, make sure to also set up in prod function listUserManalinks(fromId?: string) { return query( - manalinkCol, + manalinks, where('fromId', '==', fromId), orderBy('createdTime', 'desc') ) } export async function getManalink(slug: string) { - const docSnap = await getDoc(doc(db, 'manalinks', slug)) - return docSnap.data() as Manalink + return (await getDoc(doc(manalinks, slug))).data() } export function useManalink(slug: string) { - const [manalink, setManalink] = useState(null) + const [manalink, setManalink] = useState(undefined) useEffect(() => { if (slug) { getManalink(slug).then(setManalink) diff --git a/web/lib/firebase/txns.ts b/web/lib/firebase/txns.ts index c4c8aa93..17e9a09b 100644 --- a/web/lib/firebase/txns.ts +++ b/web/lib/firebase/txns.ts @@ -1,15 +1,14 @@ -import { ManalinkTxn, DonationTxn, TipTxn } from 'common/txn' -import { collection, orderBy, query, where } from 'firebase/firestore' -import { db } from './init' -import { getValues, listenForValues } from './utils' +import { ManalinkTxn, DonationTxn, TipTxn, Txn } from 'common/txn' +import { orderBy, query, where } from 'firebase/firestore' +import { coll, getValues, listenForValues } from './utils' import { useState, useEffect } from 'react' import { orderBy as _orderBy } from 'lodash' -const txnCollection = collection(db, 'txns') +export const txns = coll('txns') const getCharityQuery = (charityId: string) => query( - txnCollection, + txns, where('toType', '==', 'CHARITY'), where('toId', '==', charityId), orderBy('createdTime', 'desc') @@ -22,7 +21,7 @@ export function listenForCharityTxns( return listenForValues(getCharityQuery(charityId), setTxns) } -const charitiesQuery = query(txnCollection, where('toType', '==', 'CHARITY')) +const charitiesQuery = query(txns, where('toType', '==', 'CHARITY')) export function getAllCharityTxns() { return getValues(charitiesQuery) @@ -30,7 +29,7 @@ export function getAllCharityTxns() { const getTipsQuery = (contractId: string) => query( - txnCollection, + txns, where('category', '==', 'TIP'), where('data.contractId', '==', contractId) ) @@ -50,13 +49,13 @@ export function useManalinkTxns(userId: string) { useEffect(() => { // TODO: Need to instantiate these indexes too const fromQuery = query( - txnCollection, + txns, where('fromId', '==', userId), where('category', '==', 'MANALINK'), orderBy('createdTime', 'desc') ) const toQuery = query( - txnCollection, + txns, where('toId', '==', userId), where('category', '==', 'MANALINK'), orderBy('createdTime', 'desc') diff --git a/web/lib/firebase/users.ts b/web/lib/firebase/users.ts index e9fcbb93..40be6741 100644 --- a/web/lib/firebase/users.ts +++ b/web/lib/firebase/users.ts @@ -1,5 +1,4 @@ import { - getFirestore, doc, setDoc, getDoc, @@ -23,58 +22,62 @@ import { } from 'firebase/auth' import { throttle, zip } from 'lodash' -import { app } from './init' +import { app, db } from './init' import { PortfolioMetrics, PrivateUser, User } from 'common/user' import { createUser } from './fn-call' -import { getValue, getValues, listenForValue, listenForValues } from './utils' +import { + coll, + getValue, + getValues, + listenForValue, + listenForValues, +} from './utils' import { feed } from 'common/feed' import { CATEGORY_LIST } from 'common/categories' import { safeLocalStorage } from '../util/local' import { filterDefined } from 'common/util/array' +export const users = coll('users') +export const privateUsers = coll('private-users') + export type { User } export type Period = 'daily' | 'weekly' | 'monthly' | 'allTime' -const db = getFirestore(app) export const auth = getAuth(app) -export const userDocRef = (userId: string) => doc(db, 'users', userId) - export async function getUser(userId: string) { - const docSnap = await getDoc(userDocRef(userId)) - return docSnap.data() as User + /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */ + return (await getDoc(doc(users, userId))).data()! } export async function getUserByUsername(username: string) { // Find a user whose username matches the given username, or null if no such user exists. - const userCollection = collection(db, 'users') - const q = query(userCollection, where('username', '==', username), limit(1)) - const docs = await getDocs(q) - const users = docs.docs.map((doc) => doc.data() as User) - return users[0] || null + const q = query(users, where('username', '==', username), limit(1)) + const docs = (await getDocs(q)).docs + return docs.length > 0 ? docs[0].data() : null } export async function setUser(userId: string, user: User) { - await setDoc(doc(db, 'users', userId), user) + await setDoc(doc(users, userId), user) } export async function updateUser(userId: string, update: Partial) { - await updateDoc(doc(db, 'users', userId), { ...update }) + await updateDoc(doc(users, userId), { ...update }) } export async function updatePrivateUser( userId: string, update: Partial ) { - await updateDoc(doc(db, 'private-users', userId), { ...update }) + await updateDoc(doc(privateUsers, userId), { ...update }) } export function listenForUser( userId: string, setUser: (user: User | null) => void ) { - const userRef = doc(db, 'users', userId) + const userRef = doc(users, userId) return listenForValue(userRef, setUser) } @@ -82,7 +85,7 @@ export function listenForPrivateUser( userId: string, setPrivateUser: (privateUser: PrivateUser | null) => void ) { - const userRef = doc(db, 'private-users', userId) + const userRef = doc(privateUsers, userId) return listenForValue(userRef, setPrivateUser) } @@ -152,36 +155,29 @@ export async function listUsers(userIds: string[]) { if (userIds.length > 10) { throw new Error('Too many users requested at once; Firestore limits to 10') } - const userCollection = collection(db, 'users') - const q = query(userCollection, where('id', 'in', userIds)) - const docs = await getDocs(q) - return docs.docs.map((doc) => doc.data() as User) + const q = query(users, where('id', 'in', userIds)) + const docs = (await getDocs(q)).docs + return docs.map((doc) => doc.data()) } export async function listAllUsers() { - const userCollection = collection(db, 'users') - const q = query(userCollection) - const docs = await getDocs(q) - return docs.docs.map((doc) => doc.data() as User) + const docs = (await getDocs(users)).docs + return docs.map((doc) => doc.data()) } export function listenForAllUsers(setUsers: (users: User[]) => void) { - const userCollection = collection(db, 'users') - const q = query(userCollection) - listenForValues(q, setUsers) + listenForValues(users, setUsers) } export function listenForPrivateUsers( setUsers: (users: PrivateUser[]) => void ) { - const userCollection = collection(db, 'private-users') - const q = query(userCollection) - listenForValues(q, setUsers) + listenForValues(privateUsers, setUsers) } export function getTopTraders(period: Period) { const topTraders = query( - collection(db, 'users'), + users, orderBy('profitCached.' + period, 'desc'), limit(20) ) @@ -191,7 +187,7 @@ export function getTopTraders(period: Period) { export function getTopCreators(period: Period) { const topCreators = query( - collection(db, 'users'), + users, orderBy('creatorVolumeCached.' + period, 'desc'), limit(20) ) @@ -199,22 +195,21 @@ export function getTopCreators(period: Period) { } export async function getTopFollowed() { - const users = await getValues(topFollowedQuery) - return users.slice(0, 20) + return (await getValues(topFollowedQuery)).slice(0, 20) } const topFollowedQuery = query( - collection(db, 'users'), + users, orderBy('followerCountCached', 'desc'), limit(20) ) export function getUsers() { - return getValues(collection(db, 'users')) + return getValues(users) } export async function getUserFeed(userId: string) { - const feedDoc = doc(db, 'private-users', userId, 'cache', 'feed') + const feedDoc = doc(privateUsers, userId, 'cache', 'feed') const userFeed = await getValue<{ feed: feed }>(feedDoc) @@ -222,7 +217,7 @@ export async function getUserFeed(userId: string) { } export async function getCategoryFeeds(userId: string) { - const cacheCollection = collection(db, 'private-users', userId, 'cache') + const cacheCollection = collection(privateUsers, userId, 'cache') const feedData = await Promise.all( CATEGORY_LIST.map((category) => getValue<{ feed: feed }>(doc(cacheCollection, `feed-${category}`)) @@ -233,7 +228,7 @@ export async function getCategoryFeeds(userId: string) { } export async function follow(userId: string, followedUserId: string) { - const followDoc = doc(db, 'users', userId, 'follows', followedUserId) + const followDoc = doc(collection(users, userId, 'follows'), followedUserId) await setDoc(followDoc, { userId: followedUserId, timestamp: Date.now(), @@ -241,7 +236,7 @@ export async function follow(userId: string, followedUserId: string) { } export async function unfollow(userId: string, unfollowedUserId: string) { - const followDoc = doc(db, 'users', userId, 'follows', unfollowedUserId) + const followDoc = doc(collection(users, userId, 'follows'), unfollowedUserId) await deleteDoc(followDoc) } @@ -259,7 +254,7 @@ export function listenForFollows( userId: string, setFollowIds: (followIds: string[]) => void ) { - const follows = collection(db, 'users', userId, 'follows') + const follows = collection(users, userId, 'follows') return listenForValues<{ userId: string }>(follows, (docs) => setFollowIds(docs.map(({ userId }) => userId)) ) diff --git a/web/lib/firebase/utils.ts b/web/lib/firebase/utils.ts index 1a9e13c5..e63c2d96 100644 --- a/web/lib/firebase/utils.ts +++ b/web/lib/firebase/utils.ts @@ -1,10 +1,17 @@ import { + collection, getDoc, getDocs, onSnapshot, Query, + CollectionReference, DocumentReference, } from 'firebase/firestore' +import { db } from './init' + +export const coll = (path: string, ...rest: string[]) => { + return collection(db, path, ...rest) as CollectionReference +} export const getValue = async (doc: DocumentReference) => { const snap = await getDoc(doc)