Add profile fields: work areas, moral causes, and interests

This commit is contained in:
MartinBraquet
2025-12-03 16:56:02 +01:00
parent 43238ecc44
commit 3b0465c65c
31 changed files with 1134 additions and 109 deletions

View File

@@ -73,6 +73,8 @@ import {IS_LOCAL} from "common/hosting/constants";
import {editMessage} from "api/edit-message";
import {reactToMessage} from "api/react-to-message";
import {deleteMessage} from "api/delete-message";
import {updateOptions} from "api/update-options";
import {getOptions} from "api/get-options";
// const corsOptions: CorsOptions = {
// origin: ['*'], // Only allow requests from this domain
@@ -366,6 +368,8 @@ const handlers: { [k in APIPath]: APIHandler<k> } = {
'delete-message': deleteMessage,
'edit-message': editMessage,
'react-to-message': reactToMessage,
'update-options': updateOptions,
'get-options': getOptions,
// 'auth-google': authGoogle,
}

View File

@@ -0,0 +1,28 @@
import {APIError, APIHandler} from 'api/helpers/endpoint'
import {createSupabaseDirectClient} from 'shared/supabase/init'
import {log} from 'shared/utils'
import {tryCatch} from 'common/util/try-catch'
import {OPTION_TABLES} from "common/profiles/constants";
export const getOptions: APIHandler<'get-options'> = async (
{table},
_auth
) => {
if (!OPTION_TABLES.includes(table)) throw new APIError(400, 'Invalid table')
const pg = createSupabaseDirectClient()
const result = await tryCatch(
pg.manyOrNone<{ name: string }>(`SELECT interests.name
FROM interests`)
)
if (result.error) {
log('Error getting profile options', result.error)
throw new APIError(500, 'Error getting profile options')
}
const names = result.data.map(row => row.name)
return {names}
}

View File

@@ -4,6 +4,7 @@ import {createSupabaseDirectClient, pgp} from 'shared/supabase/init'
import {from, join, leftJoin, limit, orderBy, renderSql, select, where,} from 'shared/supabase/sql-builder'
import {MIN_BIO_LENGTH} from "common/constants";
import {compact} from "lodash";
import {OptionTableKey} from "common/profiles/constants";
export type profileQueryType = {
limit?: number | undefined,
@@ -37,6 +38,8 @@ export type profileQueryType = {
skipId?: string | undefined,
orderBy?: string | undefined,
lastModificationWithin?: string | undefined,
} & {
[K in OptionTableKey]?: string[] | undefined
}
// const userActivityColumns = ['last_online_time']
@@ -44,7 +47,7 @@ export type profileQueryType = {
export const loadProfiles = async (props: profileQueryType) => {
const pg = createSupabaseDirectClient()
console.debug(props)
console.debug('loadProfiles', props)
const {
limit: limitParam,
after,
@@ -66,6 +69,9 @@ export const loadProfiles = async (props: profileQueryType) => {
religion,
wants_kids_strength,
has_kids,
interests,
causes,
work,
is_smoker,
shortBio,
geodbCityIds,
@@ -95,24 +101,55 @@ export const loadProfiles = async (props: profileQueryType) => {
: 'profiles'
const userActivityJoin = 'user_activity on user_activity.user_id = profiles.user_id'
// Pre-aggregated interests per profile
function getManyToManyJoin(label: OptionTableKey) {
return `(
SELECT
profile_${label}.profile_id,
ARRAY_AGG(${label}.name ORDER BY ${label}.name) AS ${label}
FROM profile_${label}
JOIN ${label} ON ${label}.id = profile_${label}.option_id
GROUP BY profile_${label}.profile_id
) i ON i.profile_id = profiles.id`
}
const interestsJoin = getManyToManyJoin('interests')
const causesJoin = getManyToManyJoin('causes')
const workJoin = getManyToManyJoin('work')
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})
const joins = [
orderByParam === 'last_online_time' && leftJoin(userActivityJoin),
orderByParam === 'compatibility_score' && compatibleWithUserId && join(compatibilityScoreJoin),
interests && leftJoin(interestsJoin),
causes && leftJoin(causesJoin),
work && leftJoin(workJoin),
]
const _orderBy = orderByParam === 'compatibility_score' ? 'cs.score' : `${tablePrefix}.${orderByParam}`
const afterFilter = renderSql(
select(_orderBy),
from('profiles'),
orderByParam === 'last_online_time' && leftJoin(userActivityJoin),
orderByParam === 'compatibility_score' && compatibleWithUserId && join(compatibilityScoreJoin),
...joins,
where('profiles.id = $(after)', {after}),
)
const tableSelection = compact([
from('profiles'),
join('users on users.id = profiles.user_id'),
orderByParam === 'last_online_time' && leftJoin(userActivityJoin),
orderByParam === 'compatibility_score' && compatibleWithUserId && join(compatibilityScoreJoin),
...joins,
])
function getManyToManyClause(label: OptionTableKey) {
return `EXISTS (
SELECT 1 FROM profile_${label} pi2
JOIN ${label} ii2 ON ii2.id = pi2.option_id
WHERE pi2.profile_id = profiles.id
AND ii2.name = ANY (ARRAY[$(values)])
)`
}
const filters = [
where('looking_for_matches = true'),
where(`profiles.disabled != true`),
@@ -190,6 +227,12 @@ export const loadProfiles = async (props: profileQueryType) => {
{religion}
),
interests?.length && where(getManyToManyClause('interests'), {values: interests}),
causes?.length && where(getManyToManyClause('causes'), {values: causes}),
work?.length && where(getManyToManyClause('work'), {values: work}),
!!wants_kids_strength &&
wants_kids_strength !== -1 &&
where(
@@ -229,12 +272,15 @@ export const loadProfiles = async (props: profileQueryType) => {
lastModificationWithin && where(`last_modification_time >= NOW() - INTERVAL $(lastModificationWithin)`, {lastModificationWithin}),
]
let selectCols = 'profiles.*, name, username, users.data as user'
let selectCols = 'profiles.*, users.name, users.username, users.data as user'
if (orderByParam === 'compatibility_score') {
selectCols += ', cs.score as compatibility_score'
} else if (orderByParam === 'last_online_time') {
selectCols += ', user_activity.last_online_time'
}
if (interests) selectCols += `, COALESCE(i.interests, '{}') AS interests`
if (causes) selectCols += `, COALESCE(i.causes, '{}') AS causes`
if (work) selectCols += `, COALESCE(i.work, '{}') AS work`
const query = renderSql(
select(selectCols),
@@ -267,7 +313,8 @@ export const getProfiles: APIHandler<'get-profiles'> = async (props, auth) => {
if (!props.skipId) props.skipId = auth.uid
const {profiles, count} = await loadProfiles(props)
return {status: 'success', profiles: profiles, count: count}
} catch {
} catch (error) {
console.log(error)
return {status: 'fail', profiles: [], count: 0}
}
}

View File

@@ -0,0 +1,63 @@
import {APIError, APIHandler} from 'api/helpers/endpoint'
import {createSupabaseDirectClient} from 'shared/supabase/init'
import {log} from 'shared/utils'
import {tryCatch} from 'common/util/try-catch'
import {OPTION_TABLES} from "common/profiles/constants";
export const updateOptions: APIHandler<'update-options'> = async (
{table, names},
auth
) => {
if (!OPTION_TABLES.includes(table)) throw new APIError(400, 'Invalid table')
if (!names || !Array.isArray(names) || names.length === 0) {
throw new APIError(400, 'No names provided')
}
log('Updating profile options', {table, names})
const pg = createSupabaseDirectClient()
const profileIdResult = await pg.oneOrNone<{ id: number }>(
'SELECT id FROM profiles WHERE user_id = $1',
[auth.uid]
)
if (!profileIdResult) throw new APIError(404, 'Profile not found')
const profileId = profileIdResult.id
const result = await tryCatch(pg.tx(async (t) => {
const ids: number[] = []
for (const name of names) {
const row = await t.one<{ id: number }>(
`INSERT INTO ${table} (name, creator_id)
VALUES ($1, $2)
ON CONFLICT (name) DO UPDATE
SET name = ${table}.name
RETURNING id`,
[name, auth.uid]
)
ids.push(row.id)
}
// Delete old options for this profile
await t.none(`DELETE FROM profile_${table} WHERE profile_id = $1`, [profileId])
// Insert new option_ids
if (ids.length > 0) {
const values = ids.map((id, i) => `($1, $${i + 2})`).join(', ')
await t.none(
`INSERT INTO profile_${table} (profile_id, option_id) VALUES ${values}`,
[profileId, ...ids]
)
}
return ids
}))
if (result.error) {
log('Error updating profile options', result.error)
throw new APIError(500, 'Error updating profile options')
}
return {updatedIds: result.data}
}

View File

@@ -11,7 +11,7 @@ export const updateProfile: APIHandler<'update-profile'> = async (
parsedBody,
auth
) => {
log('parsedBody', parsedBody)
log('Updating profile', parsedBody)
const pg = createSupabaseDirectClient()
const { data: existingProfile } = await tryCatch(