Add Firebase schema collection helpers (kind of an RFC) (#583)

* Add Firebase schema collection helpers

* Decentralize definitions from schema file (James feedback)

* Add lint comment
This commit is contained in:
Marshall Polaris 2022-06-29 12:21:40 -07:00 committed by GitHub
parent 8132fa595b
commit 3b4666ba3e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 124 additions and 146 deletions

View File

@ -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<DocumentData, Contract>(
['contracts', contractId],
contractDocRef(contractId),
doc(contracts, contractId),
{ subscribe: true, includeMetadataChanges: true }
)

View File

@ -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<DocumentData, User>(
['users', userId],
userDocRef(userId),
doc(users, userId),
{ subscribe: true, includeMetadataChanges: true }
)

View File

@ -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<Contract>('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<Contract>
) {
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<Contract[]> {
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<Contract[]> {
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<Contract[]> {
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<Contract>(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<Contract>(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<Contract>(hotContractsQuery)
const data = await getValues<Contract>(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<Contract>(closingSoonQuery)
return sortBy(
chooseRandomSubset(contracts, 2),
(contract) => contract.closeTime
)
const data = await getValues<Contract>(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<Bet>(

View File

@ -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<Group>('groups')
export function groupPath(
groupSlug: string,
@ -23,30 +28,29 @@ export function groupPath(
}
export function updateGroup(group: Group, updates: Partial<Group>) {
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<Group>(groupCollection)
return getValues<Group>(groups)
}
export function listenForGroups(setGroups: (groups: Group[]) => void) {
return listenForValues(groupCollection, setGroups)
return listenForValues(groups, setGroups)
}
export function getGroup(groupId: string) {
return getValue<Group>(doc(groupCollection, groupId))
return getValue<Group>(doc(groups, groupId))
}
export async function getGroupBySlug(slug: string) {
const q = query(groupCollection, where('slug', '==', slug))
const groups = await getValues<Group>(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<Group>(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<Group>(q)
setGroups(groups)
const q = query(groups, where('contractIds', 'array-contains', contractId))
setGroups(await getValues<Group>(q))
}
export async function joinGroup(group: Group, userId: string): Promise<Group> {

View File

@ -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<Manalink>('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<Manalink | null>(null)
const [manalink, setManalink] = useState<Manalink | undefined>(undefined)
useEffect(() => {
if (slug) {
getManalink(slug).then(setManalink)

View File

@ -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<Txn>('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<DonationTxn>(getCharityQuery(charityId), setTxns)
}
const charitiesQuery = query(txnCollection, where('toType', '==', 'CHARITY'))
const charitiesQuery = query(txns, where('toType', '==', 'CHARITY'))
export function getAllCharityTxns() {
return getValues<DonationTxn>(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')

View File

@ -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<User>('users')
export const privateUsers = coll<PrivateUser>('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<User>) {
await updateDoc(doc(db, 'users', userId), { ...update })
await updateDoc(doc(users, userId), { ...update })
}
export async function updatePrivateUser(
userId: string,
update: Partial<PrivateUser>
) {
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<User>(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<PrivateUser>(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<User>(topFollowedQuery)
return users.slice(0, 20)
return (await getValues<User>(topFollowedQuery)).slice(0, 20)
}
const topFollowedQuery = query(
collection(db, 'users'),
users,
orderBy('followerCountCached', 'desc'),
limit(20)
)
export function getUsers() {
return getValues<User>(collection(db, 'users'))
return getValues<User>(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))
)

View File

@ -1,10 +1,17 @@
import {
collection,
getDoc,
getDocs,
onSnapshot,
Query,
CollectionReference,
DocumentReference,
} from 'firebase/firestore'
import { db } from './init'
export const coll = <T>(path: string, ...rest: string[]) => {
return collection(db, path, ...rest) as CollectionReference<T>
}
export const getValue = async <T>(doc: DocumentReference) => {
const snap = await getDoc(doc)