Skip to content

Commit 542152e

Browse files
committed
Store option IDs instead of EN labels in profiles and make keyword search match selected options
1 parent ffc966f commit 542152e

28 files changed

Lines changed: 462 additions & 219 deletions

android/app/build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ android {
88
applicationId "com.compassconnections.app"
99
minSdkVersion rootProject.ext.minSdkVersion
1010
targetSdkVersion rootProject.ext.targetSdkVersion
11-
versionCode 24
12-
versionName "1.2.0"
11+
versionCode 25
12+
versionName "1.3.0"
1313
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
1414
aaptOptions {
1515
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.

android/app/capacitor.build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
android {
44
compileOptions {
5-
sourceCompatibility JavaVersion.VERSION_21
6-
targetCompatibility JavaVersion.VERSION_21
5+
sourceCompatibility JavaVersion.VERSION_17
6+
targetCompatibility JavaVersion.VERSION_17
77
}
88
}
99

backend/api/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@compass/api",
33
"description": "Backend API endpoints",
4-
"version": "1.1.0",
4+
"version": "1.2.0",
55
"private": true,
66
"scripts": {
77
"watch:serve": "tsx watch src/serve.ts",

backend/api/src/get-profiles.ts

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export type profileQueryType = {
3838
skipId?: string | undefined,
3939
orderBy?: string | undefined,
4040
lastModificationWithin?: string | undefined,
41+
locale?: string | undefined,
4142
} & {
4243
[K in OptionTableKey]?: string[] | undefined
4344
}
@@ -82,6 +83,7 @@ export const loadProfiles = async (props: profileQueryType) => {
8283
orderBy: orderByParam = 'created_time',
8384
lastModificationWithin,
8485
skipId,
86+
locale = 'en',
8587
} = props
8688

8789
const filterLocation = lat && lon && radius
@@ -102,6 +104,10 @@ export const loadProfiles = async (props: profileQueryType) => {
102104

103105
const userActivityJoin = 'user_activity on user_activity.user_id = profiles.user_id'
104106

107+
const joinInterests = !!interests?.length
108+
const joinCauses = !!causes?.length
109+
const joinWork = !!work?.length
110+
105111
// Pre-aggregated interests per profile
106112
function getManyToManyJoin(label: OptionTableKey) {
107113
return `(
@@ -122,9 +128,9 @@ export const loadProfiles = async (props: profileQueryType) => {
122128
const joins = [
123129
orderByParam === 'last_online_time' && leftJoin(userActivityJoin),
124130
orderByParam === 'compatibility_score' && compatibleWithUserId && join(compatibilityScoreJoin),
125-
interests && leftJoin(interestsJoin),
126-
causes && leftJoin(causesJoin),
127-
work && leftJoin(workJoin),
131+
joinInterests && leftJoin(interestsJoin),
132+
joinCauses && leftJoin(causesJoin),
133+
joinWork && leftJoin(workJoin),
128134
]
129135

130136
const _orderBy = orderByParam === 'compatibility_score' ? 'cs.score' : `${tablePrefix}.${orderByParam}`
@@ -146,10 +152,23 @@ export const loadProfiles = async (props: profileQueryType) => {
146152
SELECT 1 FROM profile_${label}
147153
JOIN ${label} ON ${label}.id = profile_${label}.option_id
148154
WHERE profile_${label}.profile_id = profiles.id
149-
AND ${label}.name = ANY (ARRAY[$(values)])
155+
AND ${label}.id = ANY (ARRAY[$(values)])
150156
)`
151157
}
152158

159+
function getOptionClauseKeyword(label: OptionTableKey) {
160+
return `EXISTS (
161+
SELECT 1 FROM profile_${label}
162+
JOIN ${label} ON ${label}.id = profile_${label}.option_id
163+
LEFT JOIN ${label}_translations
164+
ON ${label}_translations.option_id = profile_${label}.option_id
165+
AND ${label}_translations.locale = $(locale)
166+
WHERE profile_${label}.profile_id = profiles.id
167+
AND lower(COALESCE(${label}_translations.name, ${label}.name)) ILIKE '%' || lower($(word)) || '%'
168+
)`
169+
}
170+
171+
153172
const filters = [
154173
where('looking_for_matches = true'),
155174
where(`profiles.disabled != true`),
@@ -160,8 +179,14 @@ export const loadProfiles = async (props: profileQueryType) => {
160179
where(`data->>'userDeleted' != 'true' or data->>'userDeleted' is null`),
161180

162181
...keywords.map(word => where(
163-
`lower(users.name) ilike '%' || lower($(word)) || '%' or lower(bio::text) ilike '%' || lower($(word)) || '%' or bio_tsv @@ phraseto_tsquery('english', $(word))`,
164-
{word}
182+
`lower(users.name) ilike '%' || lower($(word)) || '%'
183+
or lower(search_text) ilike '%' || lower($(word)) || '%'
184+
or search_tsv @@ phraseto_tsquery('english', $(word))
185+
OR ${getOptionClauseKeyword('interests')}
186+
OR ${getOptionClauseKeyword('causes')}
187+
OR ${getOptionClauseKeyword('work')}
188+
`,
189+
{word, locale}
165190
)),
166191

167192
genders?.length && where(`gender = ANY($(genders))`, {genders}),
@@ -227,7 +252,7 @@ export const loadProfiles = async (props: profileQueryType) => {
227252
{religion}
228253
),
229254

230-
interests?.length && where(getManyToManyClause('interests'), {values: interests}),
255+
interests?.length && where(getManyToManyClause('interests'), {values: interests.map(Number)}),
231256

232257
causes?.length && where(getManyToManyClause('causes'), {values: causes}),
233258

@@ -278,9 +303,9 @@ export const loadProfiles = async (props: profileQueryType) => {
278303
} else if (orderByParam === 'last_online_time') {
279304
selectCols += ', user_activity.last_online_time'
280305
}
281-
if (interests) selectCols += `, COALESCE(profile_interests.interests, '{}') AS interests`
282-
if (causes) selectCols += `, COALESCE(profile_causes.causes, '{}') AS causes`
283-
if (work) selectCols += `, COALESCE(profile_work.work, '{}') AS work`
306+
if (joinInterests) selectCols += `, COALESCE(profile_interests.interests, '{}') AS interests`
307+
if (joinCauses) selectCols += `, COALESCE(profile_causes.causes, '{}') AS causes`
308+
if (joinWork) selectCols += `, COALESCE(profile_work.work, '{}') AS work`
284309

285310
const query = renderSql(
286311
select(selectCols),

backend/api/src/update-options.ts

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,22 @@ import {tryCatch} from 'common/util/try-catch'
55
import {OPTION_TABLES} from "common/profiles/constants";
66

77
export const updateOptions: APIHandler<'update-options'> = async (
8-
{table, names},
8+
{table, values},
99
auth
1010
) => {
1111
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')
12+
if (!values || !Array.isArray(values)) {
13+
throw new APIError(400, 'No ids provided')
1414
}
1515

16-
log('Updating profile options', {table, names})
16+
const idsWithNumbers = values.map(id => {
17+
const numberId = Number(id)
18+
return isNaN(numberId) ? {isNumber: false, v: id} : {isNumber: true, v: numberId}
19+
})
20+
const names: string[] = idsWithNumbers.filter(item => !item.isNumber).map(item => item.v) as string[]
21+
const ids: number[] = idsWithNumbers.filter(item => item.isNumber).map(item => item.v) as number[]
22+
23+
log('Updating profile options', {table, ids, names})
1724

1825
const pg = createSupabaseDirectClient()
1926

@@ -25,8 +32,20 @@ export const updateOptions: APIHandler<'update-options'> = async (
2532
const profileId = profileIdResult.id
2633

2734
const result = await tryCatch(pg.tx(async (t) => {
28-
const ids: number[] = []
29-
for (const name of names) {
35+
const currentOptionsResult = await t.manyOrNone<{ id: string }>(
36+
`SELECT option_id as id
37+
FROM profile_${table}
38+
WHERE profile_id = $1`,
39+
[profileId]
40+
)
41+
const currentOptions = currentOptionsResult.map(row => row.id)
42+
if (currentOptions.sort().join(',') === ids.sort().join(',') && !names?.length) {
43+
log(`Skipping /update-${table} because they are already the same`)
44+
return undefined
45+
}
46+
47+
// Add new options
48+
for (const name of (names || [])) {
3049
const row = await t.one<{ id: number }>(
3150
`INSERT INTO ${table} (name, creator_id)
3251
VALUES ($1, $2)

backend/email/emails/functions/mock.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,8 @@ export const sinclairProfile: ProfileRow = {
110110
},
111111
bio_text: 'the futa in futarchy',
112112
bio_tsv: 'the futa in futarchy',
113+
search_text: 'the futa in futarchy',
114+
search_tsv: 'the futa in futarchy',
113115
age: 25,
114116
}
115117

@@ -221,5 +223,7 @@ export const jamesProfile: ProfileRow = {
221223
},
222224
bio_text: 'the futa in futarchy',
223225
bio_tsv: 'the futa in futarchy',
226+
search_text: 'the futa in futarchy',
227+
search_tsv: 'the futa in futarchy',
224228
age: 32,
225229
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
CREATE TABLE IF NOT EXISTS causes_translations
2+
(
3+
option_id BIGINT NOT NULL REFERENCES causes (id) ON DELETE CASCADE,
4+
locale TEXT NOT NULL, -- 'en', 'fr', 'de', etc.
5+
name TEXT NOT NULL,
6+
PRIMARY KEY (option_id, locale)
7+
);
8+
9+
-- Row Level Security
10+
ALTER TABLE causes_translations
11+
ENABLE ROW LEVEL SECURITY;
12+
13+
DROP POLICY IF EXISTS "public read" ON causes_translations;
14+
CREATE POLICY "public read" ON causes_translations
15+
FOR SELECT USING (true);
16+
17+
CREATE INDEX idx_causes_translations_option_locale
18+
ON causes_translations (option_id, locale);
19+
20+
CREATE EXTENSION IF NOT EXISTS pg_trgm;
21+
22+
CREATE INDEX idx_causes_translations_name_trgm
23+
ON causes_translations
24+
USING GIN (name gin_trgm_ops);
25+
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
CREATE TABLE IF NOT EXISTS interests_translations
2+
(
3+
option_id BIGINT NOT NULL REFERENCES interests (id) ON DELETE CASCADE,
4+
locale TEXT NOT NULL, -- 'en', 'fr', 'de', etc.
5+
name TEXT NOT NULL,
6+
PRIMARY KEY (option_id, locale)
7+
);
8+
9+
-- Row Level Security
10+
ALTER TABLE interests_translations
11+
ENABLE ROW LEVEL SECURITY;
12+
13+
DROP POLICY IF EXISTS "public read" ON interests_translations;
14+
CREATE POLICY "public read" ON interests_translations
15+
FOR SELECT USING (true);
16+
17+
DROP INDEX IF EXISTS idx_interests_translations_option_locale;
18+
CREATE INDEX idx_interests_translations_option_locale
19+
ON interests_translations (option_id, locale);
20+
21+
CREATE EXTENSION IF NOT EXISTS pg_trgm;
22+
23+
DROP INDEX IF EXISTS idx_interests_translations_name_trgm;
24+
CREATE INDEX idx_interests_translations_name_trgm
25+
ON interests_translations
26+
USING GIN (name gin_trgm_ops);
27+

backend/supabase/migration.sql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
BEGIN;
2+
\i backend/supabase/rebuild_profile_search.sql
23
\i backend/supabase/functions.sql
34
\i backend/supabase/firebase.sql
45
\i backend/supabase/profiles.sql

backend/supabase/profile_causes.sql

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,27 @@ CREATE INDEX idx_profile_causes_profile
2121

2222
CREATE INDEX idx_profile_causes_interest
2323
ON profile_causes (option_id);
24+
25+
-- Trigger to update /get-profiles search
26+
CREATE OR REPLACE FUNCTION trg_profile_causes_rebuild_search()
27+
RETURNS trigger AS
28+
$$
29+
BEGIN
30+
PERFORM rebuild_profile_search(
31+
COALESCE(NEW.profile_id, OLD.profile_id)
32+
);
33+
RETURN NULL;
34+
END;
35+
$$ LANGUAGE plpgsql;
36+
37+
CREATE TRIGGER trg_profile_causes_search_ins
38+
AFTER INSERT
39+
ON profile_causes
40+
FOR EACH ROW
41+
EXECUTE FUNCTION trg_profile_causes_rebuild_search();
42+
43+
CREATE TRIGGER trg_profile_causes_search_del
44+
AFTER DELETE
45+
ON profile_causes
46+
FOR EACH ROW
47+
EXECUTE FUNCTION trg_profile_causes_rebuild_search();

0 commit comments

Comments
 (0)