Skip to content

Commit 3b0465c

Browse files
committed
Add profile fields: work areas, moral causes, and interests
1 parent 43238ec commit 3b0465c

31 files changed

+1134
-109
lines changed

backend/api/src/app.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ import {IS_LOCAL} from "common/hosting/constants";
7373
import {editMessage} from "api/edit-message";
7474
import {reactToMessage} from "api/react-to-message";
7575
import {deleteMessage} from "api/delete-message";
76+
import {updateOptions} from "api/update-options";
77+
import {getOptions} from "api/get-options";
7678

7779
// const corsOptions: CorsOptions = {
7880
// origin: ['*'], // Only allow requests from this domain
@@ -366,6 +368,8 @@ const handlers: { [k in APIPath]: APIHandler<k> } = {
366368
'delete-message': deleteMessage,
367369
'edit-message': editMessage,
368370
'react-to-message': reactToMessage,
371+
'update-options': updateOptions,
372+
'get-options': getOptions,
369373
// 'auth-google': authGoogle,
370374
}
371375

backend/api/src/get-options.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import {APIError, APIHandler} from 'api/helpers/endpoint'
2+
import {createSupabaseDirectClient} from 'shared/supabase/init'
3+
import {log} from 'shared/utils'
4+
import {tryCatch} from 'common/util/try-catch'
5+
import {OPTION_TABLES} from "common/profiles/constants";
6+
7+
export const getOptions: APIHandler<'get-options'> = async (
8+
{table},
9+
_auth
10+
) => {
11+
if (!OPTION_TABLES.includes(table)) throw new APIError(400, 'Invalid table')
12+
13+
const pg = createSupabaseDirectClient()
14+
15+
const result = await tryCatch(
16+
pg.manyOrNone<{ name: string }>(`SELECT interests.name
17+
FROM interests`)
18+
)
19+
20+
if (result.error) {
21+
log('Error getting profile options', result.error)
22+
throw new APIError(500, 'Error getting profile options')
23+
}
24+
25+
const names = result.data.map(row => row.name)
26+
return {names}
27+
}
28+

backend/api/src/get-profiles.ts

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {createSupabaseDirectClient, pgp} from 'shared/supabase/init'
44
import {from, join, leftJoin, limit, orderBy, renderSql, select, where,} from 'shared/supabase/sql-builder'
55
import {MIN_BIO_LENGTH} from "common/constants";
66
import {compact} from "lodash";
7+
import {OptionTableKey} from "common/profiles/constants";
78

89
export type profileQueryType = {
910
limit?: number | undefined,
@@ -37,14 +38,16 @@ export type profileQueryType = {
3738
skipId?: string | undefined,
3839
orderBy?: string | undefined,
3940
lastModificationWithin?: string | undefined,
41+
} & {
42+
[K in OptionTableKey]?: string[] | undefined
4043
}
4144

4245
// const userActivityColumns = ['last_online_time']
4346

4447

4548
export const loadProfiles = async (props: profileQueryType) => {
4649
const pg = createSupabaseDirectClient()
47-
console.debug(props)
50+
console.debug('loadProfiles', props)
4851
const {
4952
limit: limitParam,
5053
after,
@@ -66,6 +69,9 @@ export const loadProfiles = async (props: profileQueryType) => {
6669
religion,
6770
wants_kids_strength,
6871
has_kids,
72+
interests,
73+
causes,
74+
work,
6975
is_smoker,
7076
shortBio,
7177
geodbCityIds,
@@ -95,24 +101,55 @@ export const loadProfiles = async (props: profileQueryType) => {
95101
: 'profiles'
96102

97103
const userActivityJoin = 'user_activity on user_activity.user_id = profiles.user_id'
104+
105+
// Pre-aggregated interests per profile
106+
function getManyToManyJoin(label: OptionTableKey) {
107+
return `(
108+
SELECT
109+
profile_${label}.profile_id,
110+
ARRAY_AGG(${label}.name ORDER BY ${label}.name) AS ${label}
111+
FROM profile_${label}
112+
JOIN ${label} ON ${label}.id = profile_${label}.option_id
113+
GROUP BY profile_${label}.profile_id
114+
) i ON i.profile_id = profiles.id`
115+
}
116+
const interestsJoin = getManyToManyJoin('interests')
117+
const causesJoin = getManyToManyJoin('causes')
118+
const workJoin = getManyToManyJoin('work')
119+
98120
const compatibilityScoreJoin = pgp.as.format(`compatibility_scores cs on (cs.user_id_1 = LEAST(profiles.user_id, $(compatibleWithUserId)) and cs.user_id_2 = GREATEST(profiles.user_id, $(compatibleWithUserId)))`, {compatibleWithUserId})
99121

122+
const joins = [
123+
orderByParam === 'last_online_time' && leftJoin(userActivityJoin),
124+
orderByParam === 'compatibility_score' && compatibleWithUserId && join(compatibilityScoreJoin),
125+
interests && leftJoin(interestsJoin),
126+
causes && leftJoin(causesJoin),
127+
work && leftJoin(workJoin),
128+
]
129+
100130
const _orderBy = orderByParam === 'compatibility_score' ? 'cs.score' : `${tablePrefix}.${orderByParam}`
101131
const afterFilter = renderSql(
102132
select(_orderBy),
103133
from('profiles'),
104-
orderByParam === 'last_online_time' && leftJoin(userActivityJoin),
105-
orderByParam === 'compatibility_score' && compatibleWithUserId && join(compatibilityScoreJoin),
134+
...joins,
106135
where('profiles.id = $(after)', {after}),
107136
)
108137

109138
const tableSelection = compact([
110139
from('profiles'),
111140
join('users on users.id = profiles.user_id'),
112-
orderByParam === 'last_online_time' && leftJoin(userActivityJoin),
113-
orderByParam === 'compatibility_score' && compatibleWithUserId && join(compatibilityScoreJoin),
141+
...joins,
114142
])
115143

144+
function getManyToManyClause(label: OptionTableKey) {
145+
return `EXISTS (
146+
SELECT 1 FROM profile_${label} pi2
147+
JOIN ${label} ii2 ON ii2.id = pi2.option_id
148+
WHERE pi2.profile_id = profiles.id
149+
AND ii2.name = ANY (ARRAY[$(values)])
150+
)`
151+
}
152+
116153
const filters = [
117154
where('looking_for_matches = true'),
118155
where(`profiles.disabled != true`),
@@ -190,6 +227,12 @@ export const loadProfiles = async (props: profileQueryType) => {
190227
{religion}
191228
),
192229

230+
interests?.length && where(getManyToManyClause('interests'), {values: interests}),
231+
232+
causes?.length && where(getManyToManyClause('causes'), {values: causes}),
233+
234+
work?.length && where(getManyToManyClause('work'), {values: work}),
235+
193236
!!wants_kids_strength &&
194237
wants_kids_strength !== -1 &&
195238
where(
@@ -229,12 +272,15 @@ export const loadProfiles = async (props: profileQueryType) => {
229272
lastModificationWithin && where(`last_modification_time >= NOW() - INTERVAL $(lastModificationWithin)`, {lastModificationWithin}),
230273
]
231274

232-
let selectCols = 'profiles.*, name, username, users.data as user'
275+
let selectCols = 'profiles.*, users.name, users.username, users.data as user'
233276
if (orderByParam === 'compatibility_score') {
234277
selectCols += ', cs.score as compatibility_score'
235278
} else if (orderByParam === 'last_online_time') {
236279
selectCols += ', user_activity.last_online_time'
237280
}
281+
if (interests) selectCols += `, COALESCE(i.interests, '{}') AS interests`
282+
if (causes) selectCols += `, COALESCE(i.causes, '{}') AS causes`
283+
if (work) selectCols += `, COALESCE(i.work, '{}') AS work`
238284

239285
const query = renderSql(
240286
select(selectCols),
@@ -267,7 +313,8 @@ export const getProfiles: APIHandler<'get-profiles'> = async (props, auth) => {
267313
if (!props.skipId) props.skipId = auth.uid
268314
const {profiles, count} = await loadProfiles(props)
269315
return {status: 'success', profiles: profiles, count: count}
270-
} catch {
316+
} catch (error) {
317+
console.log(error)
271318
return {status: 'fail', profiles: [], count: 0}
272319
}
273320
}

backend/api/src/update-options.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import {APIError, APIHandler} from 'api/helpers/endpoint'
2+
import {createSupabaseDirectClient} from 'shared/supabase/init'
3+
import {log} from 'shared/utils'
4+
import {tryCatch} from 'common/util/try-catch'
5+
import {OPTION_TABLES} from "common/profiles/constants";
6+
7+
export const updateOptions: APIHandler<'update-options'> = async (
8+
{table, names},
9+
auth
10+
) => {
11+
if (!OPTION_TABLES.includes(table)) throw new APIError(400, 'Invalid table')
12+
if (!names || !Array.isArray(names) || names.length === 0) {
13+
throw new APIError(400, 'No names provided')
14+
}
15+
16+
log('Updating profile options', {table, names})
17+
18+
const pg = createSupabaseDirectClient()
19+
20+
const profileIdResult = await pg.oneOrNone<{ id: number }>(
21+
'SELECT id FROM profiles WHERE user_id = $1',
22+
[auth.uid]
23+
)
24+
if (!profileIdResult) throw new APIError(404, 'Profile not found')
25+
const profileId = profileIdResult.id
26+
27+
const result = await tryCatch(pg.tx(async (t) => {
28+
const ids: number[] = []
29+
for (const name of names) {
30+
const row = await t.one<{ id: number }>(
31+
`INSERT INTO ${table} (name, creator_id)
32+
VALUES ($1, $2)
33+
ON CONFLICT (name) DO UPDATE
34+
SET name = ${table}.name
35+
RETURNING id`,
36+
[name, auth.uid]
37+
)
38+
ids.push(row.id)
39+
}
40+
41+
// Delete old options for this profile
42+
await t.none(`DELETE FROM profile_${table} WHERE profile_id = $1`, [profileId])
43+
44+
// Insert new option_ids
45+
if (ids.length > 0) {
46+
const values = ids.map((id, i) => `($1, $${i + 2})`).join(', ')
47+
await t.none(
48+
`INSERT INTO profile_${table} (profile_id, option_id) VALUES ${values}`,
49+
[profileId, ...ids]
50+
)
51+
}
52+
53+
return ids
54+
}))
55+
56+
if (result.error) {
57+
log('Error updating profile options', result.error)
58+
throw new APIError(500, 'Error updating profile options')
59+
}
60+
61+
return {updatedIds: result.data}
62+
}
63+

backend/api/src/update-profile.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export const updateProfile: APIHandler<'update-profile'> = async (
1111
parsedBody,
1212
auth
1313
) => {
14-
log('parsedBody', parsedBody)
14+
log('Updating profile', parsedBody)
1515
const pg = createSupabaseDirectClient()
1616

1717
const { data: existingProfile } = await tryCatch(

backend/supabase/causes.sql

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
CREATE TABLE IF NOT EXISTS causes
2+
(
3+
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
4+
creator_id text REFERENCES users (id) ON DELETE set null,
5+
name TEXT NOT NULL,
6+
CONSTRAINT causes_name_unique UNIQUE (name)
7+
);
8+
9+
-- Row Level Security
10+
ALTER TABLE causes
11+
ENABLE ROW LEVEL SECURITY;
12+
13+
DROP POLICY IF EXISTS "public read" ON causes;
14+
CREATE POLICY "public read" ON causes
15+
FOR SELECT USING (true);
16+
17+
CREATE UNIQUE INDEX idx_causes_name_ci
18+
ON causes (name);

backend/supabase/interests.sql

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
CREATE TABLE IF NOT EXISTS interests
2+
(
3+
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
4+
creator_id text REFERENCES users (id) ON DELETE set null,
5+
name TEXT NOT NULL,
6+
CONSTRAINT interests_name_unique UNIQUE (name)
7+
);
8+
9+
-- Row Level Security
10+
ALTER TABLE interests
11+
ENABLE ROW LEVEL SECURITY;
12+
13+
DROP POLICY IF EXISTS "public read" ON interests;
14+
CREATE POLICY "public read" ON interests
15+
FOR SELECT USING (true);
16+
17+
CREATE UNIQUE INDEX idx_interests_name_ci
18+
ON interests (name);
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
CREATE TABLE profile_causes
2+
(
3+
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
4+
profile_id BIGINT NOT NULL REFERENCES profiles (id) ON DELETE CASCADE,
5+
option_id BIGINT NOT NULL REFERENCES causes (id) ON DELETE CASCADE
6+
);
7+
8+
-- Row Level Security
9+
ALTER TABLE profile_causes
10+
ENABLE ROW LEVEL SECURITY;
11+
12+
DROP POLICY IF EXISTS "public read" ON profile_causes;
13+
CREATE POLICY "public read" ON profile_causes
14+
FOR SELECT USING (true);
15+
16+
ALTER TABLE profile_causes
17+
ADD CONSTRAINT profile_causes_option_unique UNIQUE (profile_id, option_id);
18+
19+
CREATE INDEX idx_profile_causes_profile
20+
ON profile_causes (profile_id);
21+
22+
CREATE INDEX idx_profile_causes_interest
23+
ON profile_causes (option_id);
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
CREATE TABLE profile_interests
2+
(
3+
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
4+
profile_id BIGINT NOT NULL REFERENCES profiles (id) ON DELETE CASCADE,
5+
option_id BIGINT NOT NULL REFERENCES interests (id) ON DELETE CASCADE
6+
);
7+
8+
-- Row Level Security
9+
ALTER TABLE profile_interests
10+
ENABLE ROW LEVEL SECURITY;
11+
12+
DROP POLICY IF EXISTS "public read" ON profile_interests;
13+
CREATE POLICY "public read" ON profile_interests
14+
FOR SELECT USING (true);
15+
16+
ALTER TABLE profile_interests
17+
ADD CONSTRAINT profile_interests_option_unique UNIQUE (profile_id, option_id);
18+
19+
CREATE INDEX idx_profile_interests_profile
20+
ON profile_interests (profile_id);
21+
22+
CREATE INDEX idx_profile_interests_interest
23+
ON profile_interests (option_id);

backend/supabase/profile_work.sql

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
CREATE TABLE profile_work
2+
(
3+
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
4+
profile_id BIGINT NOT NULL REFERENCES profiles (id) ON DELETE CASCADE,
5+
option_id BIGINT NOT NULL REFERENCES work (id) ON DELETE CASCADE
6+
);
7+
8+
-- Row Level Security
9+
ALTER TABLE profile_work
10+
ENABLE ROW LEVEL SECURITY;
11+
12+
DROP POLICY IF EXISTS "public read" ON profile_work;
13+
CREATE POLICY "public read" ON profile_work
14+
FOR SELECT USING (true);
15+
16+
ALTER TABLE profile_work
17+
ADD CONSTRAINT profile_work_option_unique UNIQUE (profile_id, option_id);
18+
19+
CREATE INDEX idx_profile_work_profile
20+
ON profile_work (profile_id);
21+
22+
CREATE INDEX idx_profile_work_interest
23+
ON profile_work (option_id);

0 commit comments

Comments
 (0)