From 5af92a7d8184564ac13ed998a0791c01a0c8eeac Mon Sep 17 00:00:00 2001 From: Ian Philips Date: Tue, 6 Sep 2022 09:24:26 -0600 Subject: [PATCH] Update groups API --- docs/docs/api.md | 12 ++++- web/lib/firebase/groups.ts | 51 +++++++++++++------ .../v0/group/by-id/{[id].ts => [id]/index.ts} | 0 web/pages/api/v0/group/by-id/[id]/markets.ts | 18 +++++++ web/pages/api/v0/groups.ts | 34 +++++++++++-- 5 files changed, 95 insertions(+), 20 deletions(-) rename web/pages/api/v0/group/by-id/{[id].ts => [id]/index.ts} (100%) create mode 100644 web/pages/api/v0/group/by-id/[id]/markets.ts diff --git a/docs/docs/api.md b/docs/docs/api.md index c02a5141..e284abdf 100644 --- a/docs/docs/api.md +++ b/docs/docs/api.md @@ -54,6 +54,10 @@ Returns the authenticated user. Gets all groups, in no particular order. +Parameters: +- `availableToUserId`: Optional. if specified, only groups that the user can + join and groups they've already joined will be returned. + Requires no authorization. ### `GET /v0/groups/[slug]` @@ -62,12 +66,18 @@ Gets a group by its slug. Requires no authorization. -### `GET /v0/groups/by-id/[id]` +### `GET /v0/group/by-id/[id]` Gets a group by its unique ID. Requires no authorization. +### `GET /v0/group/by-id/[id]/markets` + +Gets a group's markets by its unique ID. + +Requires no authorization. + ### `GET /v0/markets` Lists all markets, ordered by creation date descending. diff --git a/web/lib/firebase/groups.ts b/web/lib/firebase/groups.ts index ef67ff14..36bfe7cc 100644 --- a/web/lib/firebase/groups.ts +++ b/web/lib/firebase/groups.ts @@ -11,7 +11,7 @@ import { updateDoc, where, } from 'firebase/firestore' -import { uniq } from 'lodash' +import { uniq, uniqBy } from 'lodash' import { Group, GROUP_CHAT_SLUG, GroupLink } from 'common/group' import { coll, @@ -21,7 +21,7 @@ import { listenForValues, } from './utils' import { Contract } from 'common/contract' -import { updateContract } from 'web/lib/firebase/contracts' +import { getContractFromId, updateContract } from 'web/lib/firebase/contracts' import { db } from 'web/lib/firebase/init' import { filterDefined } from 'common/util/array' import { getUser } from 'web/lib/firebase/users' @@ -31,6 +31,9 @@ export const groupMembers = (groupId: string) => collection(groups, groupId, 'groupMembers') export const groupContracts = (groupId: string) => collection(groups, groupId, 'groupContracts') +const openGroupsQuery = query(groups, where('anyoneCanJoin', '==', true)) +const memberGroupsQuery = (userId: string) => + query(collectionGroup(db, 'groupMembers'), where('userId', '==', userId)) export function groupPath( groupSlug: string, @@ -78,23 +81,24 @@ export function listenForGroupContractDocs( return listenForValues(groupContracts(groupId), setContractDocs) } -export function listenForOpenGroups(setGroups: (groups: Group[]) => void) { - return listenForValues( - query(groups, where('anyoneCanJoin', '==', true)), - setGroups +export async function listGroupContracts(groupId: string) { + const contractDocs = await getValues<{ + contractId: string + createdTime: number + }>(groupContracts(groupId)) + return Promise.all( + contractDocs.map((doc) => getContractFromId(doc.contractId)) ) } +export function listenForOpenGroups(setGroups: (groups: Group[]) => void) { + return listenForValues(openGroupsQuery, setGroups) +} + export function getGroup(groupId: string) { return getValue(doc(groups, groupId)) } -export function getGroupContracts(groupId: string) { - return getValues<{ contractId: string; createdTime: number }>( - groupContracts(groupId) - ) -} - export async function getGroupBySlug(slug: string) { const q = query(groups, where('slug', '==', slug)) const docs = (await getDocs(q)).docs @@ -112,10 +116,7 @@ export function listenForMemberGroupIds( userId: string, setGroupIds: (groupIds: string[]) => void ) { - const q = query( - collectionGroup(db, 'groupMembers'), - where('userId', '==', userId) - ) + const q = memberGroupsQuery(userId) return onSnapshot(q, { includeMetadataChanges: true }, (snapshot) => { if (snapshot.metadata.fromCache) return @@ -136,6 +137,24 @@ export function listenForMemberGroups( }) } +export async function listAvailableGroups(userId: string) { + const [openGroups, memberGroupSnapshot] = await Promise.all([ + getValues(openGroupsQuery), + getDocs(memberGroupsQuery(userId)), + ]) + const memberGroups = filterDefined( + await Promise.all( + memberGroupSnapshot.docs.map((doc) => { + return doc.ref.parent.parent?.id + ? getGroup(doc.ref.parent.parent?.id) + : null + }) + ) + ) + + return uniqBy([...openGroups, ...memberGroups], (g) => g.id) +} + export async function addUserToGroupViaId(groupId: string, userId: string) { // get group to get the member ids const group = await getGroup(groupId) diff --git a/web/pages/api/v0/group/by-id/[id].ts b/web/pages/api/v0/group/by-id/[id]/index.ts similarity index 100% rename from web/pages/api/v0/group/by-id/[id].ts rename to web/pages/api/v0/group/by-id/[id]/index.ts diff --git a/web/pages/api/v0/group/by-id/[id]/markets.ts b/web/pages/api/v0/group/by-id/[id]/markets.ts new file mode 100644 index 00000000..f7538277 --- /dev/null +++ b/web/pages/api/v0/group/by-id/[id]/markets.ts @@ -0,0 +1,18 @@ +import { NextApiRequest, NextApiResponse } from 'next' +import { applyCorsHeaders, CORS_UNRESTRICTED } from 'web/lib/api/cors' +import { listGroupContracts } from 'web/lib/firebase/groups' + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + await applyCorsHeaders(req, res, CORS_UNRESTRICTED) + const { id } = req.query + const contracts = await listGroupContracts(id as string) + if (!contracts) { + res.status(404).json({ error: 'Group not found' }) + return + } + res.setHeader('Cache-Control', 'no-cache') + return res.status(200).json(contracts) +} diff --git a/web/pages/api/v0/groups.ts b/web/pages/api/v0/groups.ts index 84b773b3..60d94c1c 100644 --- a/web/pages/api/v0/groups.ts +++ b/web/pages/api/v0/groups.ts @@ -1,14 +1,42 @@ import type { NextApiRequest, NextApiResponse } from 'next' -import { listAllGroups } from 'web/lib/firebase/groups' +import { listAllGroups, listAvailableGroups } from 'web/lib/firebase/groups' import { applyCorsHeaders, CORS_UNRESTRICTED } from 'web/lib/api/cors' +import { z } from 'zod' +import { validate } from 'web/pages/api/v0/_validate' +import { ValidationError } from 'web/pages/api/v0/_types' -type Data = any[] +const queryParams = z + .object({ + availableToUserId: z.string().optional(), + }) + .strict() export default async function handler( req: NextApiRequest, - res: NextApiResponse + res: NextApiResponse ) { await applyCorsHeaders(req, res, CORS_UNRESTRICTED) + let params: z.infer + try { + params = validate(queryParams, req.query) + } catch (e) { + if (e instanceof ValidationError) { + return res.status(400).json(e) + } + console.error(`Unknown error during validation: ${e}`) + return res.status(500).json({ error: 'Unknown error during validation' }) + } + + const { availableToUserId } = params + + // TODO: should we check if the user is a real user? + if (availableToUserId) { + const groups = await listAvailableGroups(availableToUserId) + res.setHeader('Cache-Control', 'max-age=0') + res.status(200).json(groups) + return + } + const groups = await listAllGroups() res.setHeader('Cache-Control', 'max-age=0') res.status(200).json(groups)