Skip to content

Commit

Permalink
Topic heirarchy. New topic page (#3115)
Browse files Browse the repository at this point in the history
* Let topics have topics. New topic page (wip)

* Add controls to add/remove topics in sidebar

* use LiteGroups

* Add questions tab

* ... and topic header

* Remove topic-specific logic from /browse

* redirects

* fixup LiteGroup

* Add dashboards tab

* Add leaderboard tab

* responsive

* Style sidebar better

* only get public dashboards

* rename group -> topic

* remove static paths

* Fix about editor

* Fix remove topic ui

* sort topic sidebar by importance

* update merge groups script to delete relations

* default to questions tab for signed in
  • Loading branch information
sipec authored Nov 15, 2024
1 parent 7ee674f commit e31da5d
Show file tree
Hide file tree
Showing 30 changed files with 954 additions and 494 deletions.
47 changes: 47 additions & 0 deletions backend/api/src/add-topic-to-topic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { createSupabaseDirectClient } from 'shared/supabase/init'
import { APIError, APIHandler } from './helpers/endpoint'
import { isAdminId, isModId } from 'common/envs/constants'
import { revalidateStaticProps } from 'shared/utils'
import { groupPath } from 'common/group'

export const addOrRemoveTopicFromTopic: APIHandler<
'group/by-id/:topId/group/:bottomId'
> = async (props, auth) => {
const { topId, bottomId, remove } = props

const pg = createSupabaseDirectClient()

if (!isModId(auth.uid) && !isAdminId(auth.uid)) {
throw new APIError(
403,
'You do not have permission to update group relationships'
)
}

if (remove) {
await pg.none(
`delete from group_groups where top_id = $1 and bottom_id = $2`,
[topId, bottomId]
)
} else {
await pg.none(
`insert into group_groups (top_id, bottom_id) values ($1, $2)`,
[topId, bottomId]
)
}
const continuation = async () => {
const data = await pg.many(
`select slug from groups where id = $1 or id = $2`,
[topId, bottomId]
)
await Promise.all([
revalidateStaticProps(groupPath(data[0].slug)),
revalidateStaticProps(groupPath(data[1].slug)),
])
}

return {
result: { status: 'success' },
continue: continuation,
}
}
23 changes: 23 additions & 0 deletions backend/api/src/get-topic-dashboards.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { createSupabaseDirectClient } from 'shared/supabase/init'
import { APIHandler } from './helpers/endpoint'

export const getTopicDashboards: APIHandler<'group/:slug/dashboards'> = async ({
slug,
}) => {
const pg = createSupabaseDirectClient()

return await pg.map(
`select d.id, d.title, d.slug, d.creator_id from dashboards d
join dashboard_groups dg on d.id = dg.dashboard_id
join groups g on dg.group_id = g.id
where g.slug = $1
and d.visibility = 'public'`,
[slug],
(row) => ({
id: row.id,
title: row.title,
slug: row.slug,
creatorId: row.creator_id,
})
)
}
37 changes: 37 additions & 0 deletions backend/api/src/get-topic-topics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { createSupabaseDirectClient } from 'shared/supabase/init'
import { convertGroup } from 'common/supabase/groups'
import { APIError } from 'common/api/utils'

export const getTopicTopics = async (
props: { slug: string } | { id: string }
) => {
const pg = createSupabaseDirectClient()

let id
if ('id' in props) {
id = props.id
} else {
const group = await pg.oneOrNone(`select id from groups where slug = $1`, [
props.slug,
])
if (!group) throw new APIError(404, 'Group not found')
id = group.id
}

const [above, below] = await pg.multi(
`select id, slug, name, importance_score, privacy_status, total_members
from groups g join group_groups gg
on g.id = gg.top_id where gg.bottom_id = $1
order by importance_score desc;
select id, slug, name, importance_score, privacy_status, total_members
from groups g join group_groups gg
on g.id = gg.bottom_id where gg.top_id = $1
order by importance_score desc`,
[id]
)

return {
above: above.map(convertGroup),
below: below.map(convertGroup),
}
}
7 changes: 7 additions & 0 deletions backend/api/src/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import { getPositions } from './get-positions'
import { getLeagues } from './get-leagues'
import { getContract } from './get-contract'
import { addOrRemoveTopicFromContract } from './add-topic-to-market'
import { addOrRemoveTopicFromTopic } from './add-topic-to-topic'
import { searchUsers } from './supabase-search-users'
import {
searchMarketsLite,
Expand Down Expand Up @@ -133,6 +134,8 @@ import { toggleSystemTradingStatus } from './toggle-system-status'
import { completeCashoutRequest } from './gidx/complete-cashout-request'
import { getDailyChangedMetricsAndContracts } from './get-daily-changed-metrics-and-contracts'
import { getMarketsByIds } from './get-markets'
import { getTopicTopics } from './get-topic-topics'
import { getTopicDashboards } from './get-topic-dashboards'

// we define the handlers in this object in order to typecheck that every API has a handler
export const handlers: { [k in APIPath]: APIHandler<k> } = {
Expand Down Expand Up @@ -165,6 +168,10 @@ export const handlers: { [k in APIPath]: APIHandler<k> } = {
'group/by-id/:id/delete': deleteGroup,
'group/:slug/block': blockGroup,
'group/:slug/unblock': unblockGroup,
'group/by-id/:topId/group/:bottomId': addOrRemoveTopicFromTopic,
'group/:slug/groups': getTopicTopics,
'group/:slug/dashboards': getTopicDashboards,
'group/by-id/:id/groups': getTopicTopics,
groups: getGroups,
'market/:id': getMarket,
'market/:id/lite': ({ id }) => getMarket({ id, lite: true }),
Expand Down
33 changes: 10 additions & 23 deletions backend/scripts/merge-groups.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { SafeBulkWriter } from 'shared/safe-bulk-writer'
import { type SupabaseDirectClient } from 'shared/supabase/init'
import { bulkUpsert } from 'shared/supabase/utils'
import { runScript } from 'run-script'
import { upsertGroupEmbedding } from 'shared/helpers/embeddings'
import { updateContract } from 'shared/supabase/contracts'

// note: you should turn off the on-update-contract trigger (notifications, embedding recalculation) if it's a ton of contracts

export async function mergeGroups(
pg: SupabaseDirectClient,
firestore: any,
fromSlug: string,
toSlug: string
) {
Expand Down Expand Up @@ -42,11 +41,11 @@ export async function mergeGroups(
(row) => row.contract_id
)

// if (contracts.length > 100) {
// throw new Error(
// `found ${contracts.length} contracts in group ${from}. are you sure?`
// )
// }
if (contracts.length > 100) {
throw new Error(
`found ${contracts.length} contracts in group ${from}. are you sure?`
)
}

if (contracts.length > 0) {
console.log(`re-tagging ${contracts.length} contracts`)
Expand All @@ -63,7 +62,7 @@ export async function mergeGroups(
await pg.none('delete from group_contracts where group_id = $1', [from])

console.log('correcting contract group slugs')
await updateGroupLinksOnContracts(pg, firestore, contracts)
await updateGroupLinksOnContracts(pg, contracts)

console.log('recalculating group embedding')
await upsertGroupEmbedding(pg, to)
Expand Down Expand Up @@ -103,14 +102,9 @@ export async function mergeGroups(

export async function updateGroupLinksOnContracts(
pg: SupabaseDirectClient,
firestore: any,
contractIds: string[]
) {
const bulkWriter = new SafeBulkWriter()

for (const contractId of contractIds) {
const contractRef = firestore.collection('contracts').doc(contractId)

const groups = await pg.manyOrNone<{
group_id: string
slug: string
Expand All @@ -122,17 +116,10 @@ export async function updateGroupLinksOnContracts(
[contractId]
)

bulkWriter.update(contractRef, {
await updateContract(pg, contractId, {
groupSlugs: groups.map((g) => g.slug),
groupLinks: groups.map((g) => ({
groupId: g.group_id,
slug: g.slug,
name: g.name,
})),
})
}

await bulkWriter.flush()
}

if (require.main === module) {
Expand All @@ -141,7 +128,7 @@ if (require.main === module) {
process.exit(1)
}

runScript(async ({ pg, firestore }) => {
await mergeGroups(pg, firestore, process.argv[2], process.argv[3])
runScript(async ({ pg }) => {
await mergeGroups(pg, process.argv[2], process.argv[3])
})
}
3 changes: 1 addition & 2 deletions backend/supabase/contract_comments.sql
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@ create table if not exists
contract_id text not null,
created_time timestamp with time zone not null,
data jsonb not null,
downvotes integer default 0,
dislikes integer default 0,
likes integer default 0 not null,
upvotes integer default 0,
user_id text not null,
visibility text,
constraint primary key (contract_id, comment_id)
Expand Down
4 changes: 2 additions & 2 deletions backend/supabase/contract_embeddings.sql
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ select
using (true);

-- Indexes
drop index if exists contract_embeddings_embedding_aug_2024;
drop index if exists contract_embeddings_embedding_apr_2024;

create index contract_embeddings_embedding_aug_2024 on public.contract_embeddings using hnsw (embedding vector_cosine_ops);
create index contract_embeddings_embedding_apr_2024 on public.contract_embeddings using hnsw (embedding vector_cosine_ops);

drop index if exists contract_embeddings_pkey;

Expand Down
11 changes: 6 additions & 5 deletions backend/supabase/functions.sql
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ FROM
$function$;

create
or replace function public.firebase_uid () returns text language sql stable parallel SAFE leakproof as $function$
or replace function public.firebase_uid () returns text language sql stable parallel SAFE as $function$
select nullif(current_setting('request.jwt.claims', true)::json->>'sub', '')::text;
$function$;

Expand Down Expand Up @@ -628,12 +628,13 @@ DECLARE
-- @Austin, @JamesGrugett, @SG, @DavidChee, @Alice, @ian, @IngaWei, @mqp, @Sinclair, @ManifoldPolitics, @baraki
strings TEXT[] := ARRAY[
'igi2zGXsfxYPgB0DJTXVJVmwCOr2',
'tlmGNz9kjXc2EteizMORes4qvWl2',
'uglwf3YKOZNGjjEXKc5HampOFRE2',
'qJHrvvGfGsYiHZkGY6XjVfIMj233',
'5LZ4LgYuySdL1huCWe7bti02ghx2',
'tlmGNz9kjXc2EteizMORes4qvWl2',
'uglwf3YKOZNGjjEXKc5HampOFRE2',
'qJHrvvGfGsYiHZkGY6XjVfIMj233',
'AJwLWoo3xue32XIiAVrL5SyR1WB2', -- ian
'GRwzCexe5PM6ThrSsodKZT9ziln2',
'62TNqzdBx7X2q621HltsJm8UFht2',
'62TNqzdBx7X2q621HltsJm8UFht2',
'0k1suGSJKVUnHbCPEhHNpgZPkUP2',
'vuI5upWB8yU00rP7yxj95J2zd952',
'vUks7InCtYhBFrdLQhqXFUBHD4D2',
Expand Down
29 changes: 29 additions & 0 deletions backend/supabase/group_groups.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
-- This file is autogenerated from regen-schema.ts
create table if not exists
group_groups (
bottom_id text not null,
top_id text not null,
constraint primary key (top_id, bottom_id)
);

-- Foreign Keys
alter table group_groups
add constraint group_groups_bottom_id_fkey foreign key (bottom_id) references groups (id) on update cascade on delete cascade;

alter table group_groups
add constraint group_groups_top_id_fkey foreign key (top_id) references groups (id) on update cascade on delete cascade;

-- Row Level Security
alter table group_groups enable row level security;

-- Policies
drop policy if exists "public read" on group_groups;

create policy "public read" on group_groups for
select
using (true);

-- Indexes
drop index if exists group_groups_top_id_bottom_id_pkey;

create unique index group_groups_top_id_bottom_id_pkey on public.group_groups using btree (top_id, bottom_id);
26 changes: 12 additions & 14 deletions backend/supabase/knowledge.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
## Database Migrations and Schema Updates
@@ -18,8 +18,9 @@

#### Topics and Groups
- Topics in the UI are called "groups" in the database and code
-- Topics can have hierarchical relationships via the group_groups table
-- Topics use top/bottom terminology for hierarchical relationships, not parent/child
+- Topics can have hierarchical relationships via the group_groups table
+- Topics use top/bottom terminology for hierarchical relationships, not parent/child, to emphasize that relationships are directional but not strictly hierarchical
+- This design allows for more flexible topic organization than traditional parent/child trees, as topics can appear in multiple hierarchies
- A topic can have multiple topics above (top_id) and below (bottom_id) it
- Topics are used to categorize markets/contracts


### Applying Migrations

The project does not use a migrate script. Instead, migrations are typically applied manually by running the SQL directly against the database.

### Regenerating Types and Schema

To regenerate TypeScript types and database schema for the development environment, use the following command:

```
(cd backend/supabase && make regen-types-dev regen-schema-dev)
```

This command should be run after making changes to the database structure or when updating the development environment.
4 changes: 4 additions & 0 deletions backend/supabase/mod_reports.sql
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,7 @@ create table if not exists
drop index if exists mod_reports_pkey;

create unique index mod_reports_pkey on public.mod_reports using btree (report_id);

drop index if exists mod_status;

create index mod_status on public.mod_reports using btree (status);
44 changes: 43 additions & 1 deletion common/src/api/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,35 @@ export const API = (_apiTypeCheck = {
returns: {} as Group,
props: z.object({ slug: z.string() }),
},
'group/:slug/groups': {
method: 'GET',
visibility: 'public',
authed: false,
cache: DEFAULT_CACHE_STRATEGY,
returns: {} as { above: LiteGroup[]; below: LiteGroup[] },
props: z.object({ slug: z.string() }).strict(),
},
'group/:slug/dashboards': {
method: 'GET',
visibility: 'public',
authed: false,
cache: DEFAULT_CACHE_STRATEGY,
returns: [] as {
id: string
title: string
slug: string
creatorId: string
}[],
props: z.object({ slug: z.string() }).strict(),
},
'group/by-id/:id/groups': {
method: 'GET',
visibility: 'public',
authed: false,
cache: DEFAULT_CACHE_STRATEGY,
returns: {} as { above: LiteGroup[]; below: LiteGroup[] },
props: z.object({ id: z.string() }).strict(),
},
'group/by-id/:id': {
method: 'GET',
visibility: 'public',
Expand Down Expand Up @@ -473,7 +502,20 @@ export const API = (_apiTypeCheck = {
method: 'POST',
visibility: 'public',
authed: true,
props: z.object({ slug: z.string() }),
props: z.object({ slug: z.string() }).strict(),
},
'group/by-id/:topId/group/:bottomId': {
method: 'POST',
visibility: 'public',
authed: true,
props: z
.object({
topId: z.string(),
bottomId: z.string(),
remove: z.boolean().optional(),
})
.strict(),
returns: {} as { status: 'success' },
},
groups: {
method: 'GET',
Expand Down
Loading

0 comments on commit e31da5d

Please sign in to comment.