Backfill and forward fill contracts with group info (#686)

* Backfill and forward fill contracts with group info

* No nested queries :(

* Fix filter

* Pass empty arrays instead of undefined
This commit is contained in:
Ian Philips 2022-07-22 16:28:53 -06:00 committed by GitHub
parent 5899c1f3c0
commit 5f074206de
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 191 additions and 83 deletions

View File

@ -1,6 +1,7 @@
import { Answer } from './answer'
import { Fees } from './fees'
import { JSONContent } from '@tiptap/core'
import { GroupLink } from 'common/group'
export type AnyMechanism = DPM | CPMM
export type AnyOutcomeType = Binary | PseudoNumeric | FreeResponse | Numeric
@ -46,6 +47,7 @@ export type Contract<T extends AnyContractType = AnyContractType> = {
collectedFees: Fees
groupSlugs?: string[]
groupLinks?: GroupLink[]
uniqueBettorIds?: string[]
uniqueBettorCount?: number
popularityScore?: number

View File

@ -19,3 +19,11 @@ export const MAX_ABOUT_LENGTH = 140
export const MAX_ID_LENGTH = 60
export const NEW_USER_GROUP_SLUGS = ['updates', 'bugs', 'welcome']
export const GROUP_CHAT_SLUG = 'chat'
export type GroupLink = {
slug: string
name: string
groupId: string
createdTime: number
userId?: string
}

View File

@ -74,7 +74,7 @@ service cloud.firestore {
match /contracts/{contractId} {
allow read;
allow update: if request.resource.data.diff(resource.data).affectedKeys()
.hasOnly(['tags', 'lowercaseTags', 'groupSlugs']);
.hasOnly(['tags', 'lowercaseTags', 'groupSlugs', 'groupLinks']);
allow update: if request.resource.data.diff(resource.data).affectedKeys()
.hasOnly(['description', 'closeTime', 'question'])
&& resource.data.creatorId == request.auth.uid;

View File

@ -3,6 +3,7 @@ import * as admin from 'firebase-admin'
import { Group } from 'common/group'
import { Contract } from 'common/contract'
const firestore = admin.firestore()
export const onDeleteGroup = functions.firestore
@ -15,17 +16,21 @@ export const onDeleteGroup = functions.firestore
.collection('contracts')
.where('groupSlugs', 'array-contains', group.slug)
.get()
console.log("contracts with group's slug:", contracts)
for (const doc of contracts.docs) {
const contract = doc.data() as Contract
const newGroupLinks = contract.groupLinks?.filter(
(link) => link.slug !== group.slug
)
// remove the group from the contract
await firestore
.collection('contracts')
.doc(contract.id)
.update({
groupSlugs: (contract.groupSlugs ?? []).filter(
(groupSlug) => groupSlug !== group.slug
),
groupSlugs: contract.groupSlugs?.filter((s) => s !== group.slug),
groupLinks: newGroupLinks ?? [],
})
}
})

View File

@ -3,7 +3,7 @@ import * as admin from 'firebase-admin'
import { initAdmin } from './script-init'
import { getValues, isProd } from '../utils'
import { CATEGORIES_GROUP_SLUG_POSTFIX } from 'common/categories'
import { Group } from 'common/group'
import { Group, GroupLink } from 'common/group'
import { uniq } from 'lodash'
import { Contract } from 'common/contract'
import { User } from 'common/user'
@ -17,27 +17,6 @@ initAdmin()
const adminFirestore = admin.firestore()
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const addGroupIdToContracts = async () => {
const groups = await getValues<Group>(adminFirestore.collection('groups'))
const contracts = await getValues<Contract>(
adminFirestore.collection('contracts')
)
for (const group of groups) {
const groupContracts = contracts.filter((contract) =>
group.contractIds.includes(contract.id)
)
for (const contract of groupContracts) {
await adminFirestore
.collection('contracts')
.doc(contract.id)
.update({
groupSlugs: uniq([...(contract.groupSlugs ?? []), group.slug]),
})
}
}
}
const convertCategoriesToGroupsInternal = async (categories: string[]) => {
for (const category of categories) {
const markets = await getValues<Contract>(
@ -93,18 +72,30 @@ const convertCategoriesToGroupsInternal = async (categories: string[]) => {
})
for (const market of markets) {
if (market.groupLinks?.map((l) => l.groupId).includes(newGroup.id))
continue // already in that group
const newGroupLinks = [
...(market.groupLinks ?? []),
{
groupId: newGroup.id,
createdTime: Date.now(),
slug: newGroup.slug,
name: newGroup.name,
} as GroupLink,
]
await adminFirestore
.collection('contracts')
.doc(market.id)
.update({
groupSlugs: uniq([...(market?.groupSlugs ?? []), newGroup.slug]),
groupSlugs: uniq([...(market.groupSlugs ?? []), newGroup.slug]),
groupLinks: newGroupLinks,
})
}
}
}
async function convertCategoriesToGroups() {
// await addGroupIdToContracts()
// const defaultCategories = Object.values(DEFAULT_CATEGORIES)
const moreCategories = ['world', 'culture']
await convertCategoriesToGroupsInternal(moreCategories)

View File

@ -0,0 +1,52 @@
import { getValues } from 'functions/src/utils'
import { Group } from 'common/group'
import { Contract } from 'common/contract'
import { initAdmin } from 'functions/src/scripts/script-init'
import * as admin from 'firebase-admin'
import { filterDefined } from 'common/util/array'
import { uniq } from 'lodash'
initAdmin()
const adminFirestore = admin.firestore()
const addGroupIdToContracts = async () => {
const groups = await getValues<Group>(adminFirestore.collection('groups'))
const contracts = await getValues<Contract>(
adminFirestore.collection('contracts')
)
for (const group of groups) {
const groupContracts = contracts.filter((contract) =>
group.contractIds.includes(contract.id)
)
for (const contract of groupContracts) {
const oldGroupLinks = contract.groupLinks?.filter(
(l) => l.slug != group.slug
)
const newGroupLinks = filterDefined([
...(oldGroupLinks ?? []),
group.id
? {
slug: group.slug,
name: group.name,
groupId: group.id,
createdTime: Date.now(),
}
: undefined,
])
await adminFirestore
.collection('contracts')
.doc(contract.id)
.update({
groupSlugs: uniq([...(contract.groupSlugs ?? []), group.slug]),
groupLinks: newGroupLinks,
})
}
}
}
if (require.main === module) {
addGroupIdToContracts()
.then(() => process.exit())
.catch(console.log)
}

View File

@ -23,11 +23,8 @@ import { useState } from 'react'
import { ContractInfoDialog } from './contract-info-dialog'
import { Bet } from 'common/bet'
import NewContractBadge from '../new-contract-badge'
import { CATEGORY_LIST } from 'common/categories'
import { TagsList } from '../tags-list'
import { UserFollowButton } from '../follow-button'
import { DAY_MS } from 'common/util/time'
import { useGroupsWithContract } from 'web/hooks/use-group'
import { ShareIconButton } from 'web/components/share-icon-button'
import { useUser } from 'web/hooks/use-user'
import { Editor } from '@tiptap/react'
@ -37,6 +34,8 @@ import { Button } from 'web/components/button'
import { Modal } from 'web/components/layout/modal'
import { Col } from 'web/components/layout/col'
import { ContractGroupsList } from 'web/components/groups/contract-groups-list'
import { SiteLink } from 'web/components/site-link'
import { groupPath } from 'web/lib/firebase/groups'
export type ShowTime = 'resolve-date' | 'close-date'
@ -50,15 +49,16 @@ export function MiscDetails(props: {
volume,
volume24Hours,
closeTime,
tags,
isResolved,
createdTime,
resolutionTime,
groupLinks,
} = contract
// Show at most one category that this contract is tagged by
const categories = CATEGORY_LIST.filter((category) =>
tags.map((t) => t.toLowerCase()).includes(category)
).slice(0, 1)
// const categories = CATEGORY_LIST.filter((category) =>
// tags.map((t) => t.toLowerCase()).includes(category)
// ).slice(0, 1)
const isNew = createdTime > Date.now() - DAY_MS && !isResolved
return (
@ -80,13 +80,24 @@ export function MiscDetails(props: {
{fromNow(resolutionTime || 0)}
</Row>
) : volume > 0 || !isNew ? (
<Row>{contractPool(contract)} pool</Row>
<Row className={'shrink-0'}>{contractPool(contract)} pool</Row>
) : (
<NewContractBadge />
)}
{categories.length > 0 && (
<TagsList className="text-gray-400" tags={categories} noLabel />
{/*{categories.length > 0 && (*/}
{/* <TagsList className="text-gray-400" tags={categories} noLabel />*/}
{/*)}*/}
{groupLinks && groupLinks.length > 0 && (
<SiteLink
href={groupPath(groupLinks[0].slug)}
className="text-sm text-gray-400"
>
<Row className={'line-clamp-1 flex-wrap items-center '}>
<UserGroupIcon className="mx-1 mb-0.5 inline h-4 w-4 shrink-0" />
{groupLinks[0].name}
</Row>
</SiteLink>
)}
</Row>
)
@ -134,11 +145,12 @@ export function ContractDetails(props: {
disabled?: boolean
}) {
const { contract, bets, isCreator, disabled } = props
const { closeTime, creatorName, creatorUsername, creatorId } = contract
const { closeTime, creatorName, creatorUsername, creatorId, groupLinks } =
contract
const { volumeLabel, resolvedDate } = contractMetrics(contract)
const groups = useGroupsWithContract(contract)
const groupToDisplay = groups[0] ?? null
const groupToDisplay =
groupLinks?.sort((a, b) => a.createdTime - b.createdTime)[0] ?? null
const user = useUser()
const [open, setOpen] = useState(false)
@ -172,11 +184,7 @@ export function ContractDetails(props: {
<Row>
<UserGroupIcon className="mx-1 inline h-5 w-5 shrink-0" />
<span className={'line-clamp-1'}>
{contract.groupSlugs && !groupToDisplay
? ''
: groupToDisplay
? groupToDisplay.name
: 'No group'}
{groupToDisplay ? groupToDisplay.name : 'No group'}
</span>
</Row>
</Button>
@ -187,7 +195,11 @@ export function ContractDetails(props: {
'max-h-[70vh] min-h-[20rem] overflow-auto rounded bg-white p-6'
}
>
<ContractGroupsList groups={groups} contract={contract} user={user} />
<ContractGroupsList
groupLinks={groupLinks ?? []}
contract={contract}
user={user}
/>
</Col>
</Modal>

View File

@ -1,8 +1,7 @@
import { Group } from 'common/group'
import { Col } from 'web/components/layout/col'
import { Row } from 'web/components/layout/row'
import clsx from 'clsx'
import { GroupLink } from 'web/pages/groups'
import { GroupLinkItem } from 'web/pages/groups'
import { XIcon } from '@heroicons/react/outline'
import { Button } from 'web/components/button'
import { GroupSelector } from 'web/components/groups/group-selector'
@ -13,14 +12,16 @@ import {
import { User } from 'common/user'
import { Contract } from 'common/contract'
import { SiteLink } from 'web/components/site-link'
import { GroupLink } from 'common/group'
import { useGroupsWithContract } from 'web/hooks/use-group'
export function ContractGroupsList(props: {
groups: Group[]
groupLinks: GroupLink[]
contract: Contract
user: User | null | undefined
}) {
const { groups, user, contract } = props
const { groupLinks, user, contract } = props
const groups = useGroupsWithContract(contract)
return (
<Col className={'gap-2'}>
<span className={'text-xl text-indigo-700'}>
@ -33,10 +34,10 @@ export function ContractGroupsList(props: {
options={{
showSelector: true,
showLabel: false,
ignoreGroupIds: groups.map((g) => g.id),
ignoreGroupIds: groupLinks.map((g) => g.groupId),
}}
setSelectedGroup={(group) =>
group && addContractToGroup(group, contract)
group && addContractToGroup(group, contract, user.id)
}
selectedGroup={undefined}
creator={user}
@ -54,7 +55,7 @@ export function ContractGroupsList(props: {
className={clsx('items-center justify-between gap-2 p-2')}
>
<Row className="line-clamp-1 items-center gap-2">
<GroupLink group={group} />
<GroupLinkItem group={group} />
</Row>
{user && group.memberIds.includes(user.id) && (
<Button

View File

@ -11,7 +11,7 @@ import { Modal } from 'web/components/layout/modal'
import { Col } from 'web/components/layout/col'
import { joinGroup, leaveGroup } from 'web/lib/firebase/groups'
import { firebaseLogin } from 'web/lib/firebase/users'
import { GroupLink } from 'web/pages/groups'
import { GroupLinkItem } from 'web/pages/groups'
import toast from 'react-hot-toast'
export function GroupsButton(props: { user: User }) {
@ -77,7 +77,7 @@ function GroupItem(props: { group: Group; className?: string }) {
return (
<Row className={clsx('items-center justify-between gap-2 p-2', className)}>
<Row className="line-clamp-1 items-center gap-2">
<GroupLink group={group} />
<GroupLinkItem group={group} />
</Row>
<JoinOrLeaveGroupButton group={group} />
</Row>

View File

@ -1,17 +1,17 @@
import dayjs from 'dayjs'
import {
doc,
setDoc,
deleteDoc,
where,
collection,
query,
getDocs,
orderBy,
deleteDoc,
doc,
getDoc,
updateDoc,
getDocs,
limit,
orderBy,
query,
setDoc,
startAfter,
updateDoc,
where,
} from 'firebase/firestore'
import { sortBy, sum } from 'lodash'
@ -129,6 +129,7 @@ export async function listContractsByGroupSlug(
): Promise<Contract[]> {
const q = query(contracts, where('groupSlugs', 'array-contains', slug))
const snapshot = await getDocs(q)
console.log(snapshot.docs.map((doc) => doc.data()))
return snapshot.docs.map((doc) => doc.data())
}

View File

@ -7,7 +7,7 @@ import {
where,
} from 'firebase/firestore'
import { sortBy, uniq } from 'lodash'
import { Group, GROUP_CHAT_SLUG } from 'common/group'
import { Group, GROUP_CHAT_SLUG, GroupLink } from 'common/group'
import { updateContract } from './contracts'
import {
coll,
@ -124,9 +124,27 @@ export async function leaveGroup(group: Group, userId: string): Promise<void> {
return await updateGroup(group, { memberIds: uniq(newMemberIds) })
}
export async function addContractToGroup(group: Group, contract: Contract) {
export async function addContractToGroup(
group: Group,
contract: Contract,
userId: string
) {
if (contract.groupLinks?.map((l) => l.groupId).includes(group.id)) return // already in that group
const newGroupLinks = [
...(contract.groupLinks ?? []),
{
groupId: group.id,
createdTime: Date.now(),
slug: group.slug,
userId,
name: group.name,
} as GroupLink,
]
await updateContract(contract.id, {
groupSlugs: [...(contract.groupSlugs ?? []), group.slug],
groupSlugs: uniq([...(contract.groupSlugs ?? []), group.slug]),
groupLinks: newGroupLinks,
})
return await updateGroup(group, {
contractIds: uniq([...group.contractIds, contract.id]),
@ -142,11 +160,15 @@ export async function removeContractFromGroup(
group: Group,
contract: Contract
) {
const newGroupSlugs = contract.groupSlugs?.filter(
(slug) => slug !== group.slug
if (!contract.groupLinks?.map((l) => l.groupId).includes(group.id)) return // not in that group
const newGroupLinks = contract.groupLinks?.filter(
(link) => link.slug !== group.slug
)
await updateContract(contract.id, {
groupSlugs: uniq(newGroupSlugs ?? []),
groupSlugs:
contract.groupSlugs?.filter((slug) => slug !== group.slug) ?? [],
groupLinks: newGroupLinks ?? [],
})
const newContractIds = group.contractIds.filter((id) => id !== contract.id)
return await updateGroup(group, {
@ -159,8 +181,22 @@ export async function removeContractFromGroup(
})
}
export async function setContractGroupSlugs(group: Group, contractId: string) {
await updateContract(contractId, { groupSlugs: [group.slug] })
export async function setContractGroupLinks(
group: Group,
contractId: string,
userId: string
) {
await updateContract(contractId, {
groupLinks: [
{
groupId: group.id,
name: group.name,
slug: group.slug,
userId,
createdTime: Date.now(),
} as GroupLink,
],
})
return await updateGroup(group, {
contractIds: uniq([...group.contractIds, contractId]),
})

View File

@ -19,7 +19,7 @@ import {
import { formatMoney } from 'common/util/format'
import { removeUndefinedProps } from 'common/util/object'
import { ChoicesToggleGroup } from 'web/components/choices-toggle-group'
import { getGroup, setContractGroupSlugs } from 'web/lib/firebase/groups'
import { getGroup, setContractGroupLinks } from 'web/lib/firebase/groups'
import { Group } from 'common/group'
import { useTracking } from 'web/hooks/use-tracking'
import { useWarnUnsavedChanges } from 'web/hooks/use-warn-unsaved-changes'
@ -226,7 +226,7 @@ export function NewContract(props: {
isFree: false,
})
if (result && selectedGroup) {
await setContractGroupSlugs(selectedGroup, result.id)
await setContractGroupLinks(selectedGroup, result.id, creator.id)
}
await router.push(contractPath(result as Contract))

View File

@ -1,4 +1,4 @@
import { take, sortBy, debounce } from 'lodash'
import { debounce, sortBy, take } from 'lodash'
import PlusSmIcon from '@heroicons/react/solid/PlusSmIcon'
import { Group, GROUP_CHAT_SLUG } from 'common/group'
@ -6,11 +6,11 @@ import { Page } from 'web/components/page'
import { listAllBets } from 'web/lib/firebase/bets'
import { Contract, listContractsByGroupSlug } from 'web/lib/firebase/contracts'
import {
groupPath,
getGroupBySlug,
updateGroup,
joinGroup,
addContractToGroup,
getGroupBySlug,
groupPath,
joinGroup,
updateGroup,
} from 'web/lib/firebase/groups'
import { Row } from 'web/components/layout/row'
import { UserLink } from 'web/components/user-page'
@ -543,7 +543,7 @@ function AddContractButton(props: { group: Group; user: User }) {
const [open, setOpen] = useState(false)
async function addContractToCurrentGroup(contract: Contract) {
await addContractToGroup(group, contract)
await addContractToGroup(group, contract, user.id)
setOpen(false)
}

View File

@ -1,4 +1,4 @@
import { sortBy, debounce } from 'lodash'
import { debounce, sortBy } from 'lodash'
import Link from 'next/link'
import React, { useEffect, useState } from 'react'
import { Group } from 'common/group'
@ -238,7 +238,7 @@ function GroupMembersList(props: { group: Group }) {
)
}
export function GroupLink(props: { group: Group; className?: string }) {
export function GroupLinkItem(props: { group: Group; className?: string }) {
const { group, className } = props
return (