Massive upgrade to the API Swagger UI @ api.compassmeet.com

This commit is contained in:
MartinBraquet
2025-10-25 03:42:23 +02:00
parent f483ae42a8
commit 0283eb4d85
8 changed files with 386 additions and 141 deletions

View File

@@ -2,28 +2,7 @@
"openapi": "3.0.0",
"info": {
"title": "Compass API",
"version": "1.0.0"
"version": "dynamically set in app.ts"
},
"paths": {
"/health": {
"get": {
"summary": "Health",
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/get-profiles": {
"get": {
"summary": "List profiles",
"responses": {
"200": {
"description": "OK"
}
}
}
}
}
"paths": {}
}

View File

@@ -20,7 +20,6 @@ import {getLikesAndShips} from './get-likes-and-ships'
import {getProfileAnswers} from './get-profile-answers'
import {getProfiles} from './get-profiles'
import {getSupabaseToken} from './get-supabase-token'
import {getDisplayUser, getUser} from './get-user'
import {getMe} from './get-me'
import {hasFreeLike} from './has-free-like'
import {health} from './health'
@@ -53,7 +52,6 @@ import {getNotifications} from './get-notifications'
import {updateNotifSettings} from './update-notif-setting'
import {setLastOnlineTime} from './set-last-online-time'
import swaggerUi from "swagger-ui-express"
import * as fs from "fs"
import {sendSearchNotifications} from "api/send-search-notifications";
import {sendDiscordMessage} from "common/discord/core";
import {getMessagesCount} from "api/get-messages-count";
@@ -63,6 +61,10 @@ import {contact} from "api/contact";
import {saveSubscription} from "api/save-subscription";
import {createBookmarkedSearch} from './create-bookmarked-search'
import {deleteBookmarkedSearch} from './delete-bookmarked-search'
import {OpenAPIV3} from 'openapi-types';
import {version as pkgVersion} from './../package.json'
import {z, ZodFirstPartyTypeKind, ZodTypeAny} from "zod";
import {getUser} from "api/get-user";
// const corsOptions: CorsOptions = {
// origin: ['*'], // Only allow requests from this domain
@@ -117,17 +119,182 @@ const apiErrorHandler: ErrorRequestHandler = (error, _req, res, _next) => {
export const app = express()
app.use(requestMonitoring)
const swaggerDocument = JSON.parse(fs.readFileSync("./openapi.json", "utf-8"))
swaggerDocument.info = {
...swaggerDocument.info,
description: "Compass is a free, open-source platform to help people form deep, meaningful, and lasting connections — whether platonic, romantic, or collaborative. Its made possible by contributions from the community, including code, ideas, feedback, and donations. Unlike typical apps, Compass prioritizes values, interests, and personality over swipes and ads, giving you full control over who you discover and how you connect.",
version: "1.0.0",
contact: {
name: "Compass",
email: "hello@compassmeet.com",
url: "https://compassmeet.com"
const schemaCache = new WeakMap<ZodTypeAny, any>();
export function zodToOpenApiSchema(
zodObj: ZodTypeAny,
nameHint?: string
): any { // Prevent infinite recursion
if (schemaCache.has(zodObj)) {
return schemaCache.get(zodObj);
}
};
const def: any = (zodObj as any)._def;
const typeName = def.typeName as ZodFirstPartyTypeKind;
// Placeholder so recursive references can point here
const placeholder: any = {};
schemaCache.set(zodObj, placeholder);
let schema: any;
switch (typeName) {
case 'ZodString':
schema = { type: 'string' };
break;
case 'ZodNumber':
schema = { type: 'number' };
break;
case 'ZodBoolean':
schema = { type: 'boolean' };
break;
case 'ZodEnum':
schema = { type: 'string', enum: def.values };
break;
case 'ZodArray':
schema = { type: 'array', items: zodToOpenApiSchema(def.type) };
break;
case 'ZodObject': {
const shape = def.shape();
const properties: Record<string, any> = {};
const required: string[] = [];
for (const key in shape) {
const child = shape[key];
properties[key] = zodToOpenApiSchema(child, key);
if (!child.isOptional()) required.push(key);
}
schema = {
type: 'object',
properties,
...(required.length ? { required } : {}),
};
break;
}
case 'ZodRecord':
schema = {
type: 'object',
additionalProperties: zodToOpenApiSchema(def.valueType),
};
break;
case 'ZodIntersection': {
const left = zodToOpenApiSchema(def.left);
const right = zodToOpenApiSchema(def.right);
schema = { allOf: [left, right] };
break;
}
case 'ZodLazy':
// Recursive schema: use a $ref placeholder name
schema = {
$ref: `#/components/schemas/${nameHint ?? 'RecursiveType'}`,
};
break;
case 'ZodUnion':
schema = {
oneOf: def.options.map((opt: ZodTypeAny) => zodToOpenApiSchema(opt)),
};
break;
default:
schema = { type: 'string' }; // fallback for unhandled
}
Object.assign(placeholder, schema);
return schema;
}
function generateSwaggerPaths(api: typeof API) {
const paths: Record<string, any> = {};
for (const [route, config] of Object.entries(api)) {
const pathKey = '/' + route.replace(/_/g, '-'); // optional: convert underscores to dashes
const method = config.method.toLowerCase();
const summary = (config as any).summary ?? route;
// Include props in request body for POST/PUT
const operation: any = {
summary,
tags: [(config as any).tag ?? 'API'],
responses: {
200: {
description: 'OK',
content: {
'application/json': {
schema: {type: 'object'}, // could be improved by introspecting returns
},
},
},
},
};
// Include props in request body for POST/PUT
if (config.props && ['post', 'put', 'patch'].includes(method)) {
operation.requestBody = {
required: true,
content: {
'application/json': {
schema: zodToOpenApiSchema(config.props),
},
},
};
}
// Include props as query parameters for GET/DELETE
if (config.props && ['get', 'delete'].includes(method)) {
const shape = (config.props as z.ZodObject<any>)._def.shape();
operation.parameters = Object.entries(shape).map(([key, zodType]) => {
const typeMap: Record<string, string> = {
ZodString: 'string',
ZodNumber: 'number',
ZodBoolean: 'boolean',
};
const t = zodType as z.ZodTypeAny; // assert type to ZodTypeAny
return {
name: key,
in: 'query',
required: !(t.isOptional ?? false),
schema: {type: typeMap[t._def.typeName] ?? 'string'},
};
});
}
paths[pathKey] = {
[method]: operation,
}
if (config.authed) {
operation.security = [{BearerAuth: []}];
}
}
return paths;
}
const swaggerDocument: OpenAPIV3.Document = {
openapi: "3.0.0",
info: {
title: "Compass API",
description: "Compass is a free, open-source platform to help people form deep, meaningful, and lasting connections — whether platonic, romantic, or collaborative. Its made possible by contributions from the community, including code, ideas, feedback, and donations. Unlike typical apps, Compass prioritizes values, interests, and personality over swipes and ads, giving you full control over who you discover and how you connect.",
version: pkgVersion,
contact: {
name: "Compass",
email: "hello@compassmeet.com",
url: "https://compassmeet.com"
}
},
paths: generateSwaggerPaths(API),
components: {
securitySchemes: {
BearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
},
},
}
} as OpenAPIV3.Document;
const rootPath = pathWithPrefix("/")
app.get(rootPath, swaggerUi.setup(swaggerDocument))
@@ -142,10 +309,10 @@ const handlers: { [k in APIPath]: APIHandler<k> } = {
'get-supabase-token': getSupabaseToken,
'get-notifications': getNotifications,
'mark-all-notifs-read': markAllNotifsRead,
'user/:username': getUser,
'user/:username/lite': getDisplayUser,
// 'user/:username': getUser,
// 'user/:username/lite': getDisplayUser,
'user/by-id/:id': getUser,
'user/by-id/:id/lite': getDisplayUser,
// 'user/by-id/:id/lite': getDisplayUser,
'user/by-id/:id/block': blockUser,
'user/by-id/:id/unblock': unblockUser,
'search-users': searchUsers,
@@ -218,8 +385,6 @@ Object.entries(handlers).forEach(([path, handler]) => {
}
})
// console.debug('COMPASS_API_KEY:', process.env.COMPASS_API_KEY)
// Internal Endpoints
app.post(pathWithPrefix("/internal/send-search-notifications"),
async (req, res) => {

View File

@@ -17,17 +17,18 @@ export const getUser = async (props: { id: string } | { username: string }) => {
return toUserAPIResponse(user)
}
export const getDisplayUser = async (
props: { id: string } | { username: string }
) => {
const pg = createSupabaseDirectClient()
const liteUser = await pg.oneOrNone(
`select ${displayUserColumns}
from users
where ${'id' in props ? 'id' : 'username'} = $1`,
['id' in props ? props.id : props.username]
)
if (!liteUser) throw new APIError(404, 'User not found')
return removeNullOrUndefinedProps(liteUser)
}
// export const getDisplayUser = async (
// props: { id: string } | { username: string }
// ) => {
// console.log('getDisplayUser', props)
// const pg = createSupabaseDirectClient()
// const liteUser = await pg.oneOrNone(
// `select ${displayUserColumns}
// from users
// where ${'id' in props ? 'id' : 'username'} = $1`,
// ['id' in props ? props.id : props.username]
// )
// if (!liteUser) throw new APIError(404, 'User not found')
//
// return removeNullOrUndefinedProps(liteUser)
// }

View File

@@ -8,6 +8,7 @@
"tsBuildInfoFile": "lib/tsconfig.tsbuildinfo",
"sourceMap": true,
"strict": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"target": "esnext",

View File

@@ -11,7 +11,7 @@ export {SupabaseClient} from 'common/supabase/utils'
export const pgp = pgPromise({
error(err: any, e: pgPromise.IEventContext) {
// Read more: https://node-postgres.com/apis/pool#error
log.error('pgPromise background error', {
log.error(`pgPromise background error: ${err?.detail}`, {
error: err,
event: e,
})

View File

@@ -1,9 +1,4 @@
import {
contentSchema,
combinedProfileSchema,
baseProfilesSchema,
arraybeSchema, zBoolean,
} from 'common/api/zod-types'
import {arraybeSchema, baseProfilesSchema, combinedProfileSchema, contentSchema, zBoolean,} from 'common/api/zod-types'
import {PrivateChatMessage} from 'common/chat-message'
import {CompatibilityScore} from 'common/profiles/compatibility-score'
import {MAX_COMPATIBILITY_QUESTION_LENGTH} from 'common/profiles/constants'
@@ -12,7 +7,7 @@ import {Row} from 'common/supabase/utils'
import {PrivateUser, User} from 'common/user'
import {z} from 'zod'
import {LikeData, ShipData} from './profile-types'
import {DisplayUser, FullUser} from './user-types'
import {FullUser} from './user-types'
import {PrivateMessageChannel} from 'common/supabase/private-messages'
import {Notification} from 'common/notifications'
import {arrify} from 'common/util/array'
@@ -36,6 +31,10 @@ type APIGenericSchema = {
returns?: Record<string, any>
// Cache-Control header. like, 'max-age=60'
cache?: string
// Description of the endpoint
summary?: string
// Tag for grouping endpoints in documentation
tag?: string
}
let _apiTypeCheck: { [x: string]: APIGenericSchema }
@@ -47,6 +46,8 @@ export const API = (_apiTypeCheck = {
rateLimited: false,
props: z.object({}),
returns: {} as { message: 'Server is working.'; uid?: string },
summary: 'Check whether the API server is running',
tag: 'General',
},
'get-supabase-token': {
method: 'GET',
@@ -54,24 +55,69 @@ export const API = (_apiTypeCheck = {
rateLimited: false,
props: z.object({}),
returns: {} as { jwt: string },
summary: 'Return a Supabase JWT for authenticated clients',
tag: 'Tokens',
},
'mark-all-notifs-read': {
method: 'POST',
authed: true,
rateLimited: false,
props: z.object({}),
summary: 'Mark all user notifications as read',
tag: 'Notifications',
},
// 'user/:username': {
// method: 'GET',
// authed: false,
// rateLimited: false,
// cache: DEFAULT_CACHE_STRATEGY,
// returns: {} as FullUser,
// props: z.object({username: z.string()}).strict(),
// summary: 'Get full public profile by username',
// },
// 'user/:username/lite': {
// method: 'GET',
// authed: false,
// rateLimited: false,
// cache: DEFAULT_CACHE_STRATEGY,
// returns: {} as DisplayUser,
// props: z.object({username: z.string()}).strict(),
// summary: 'Get lightweight public profile by username',
// },
'user/by-id/:id': {
method: 'GET',
authed: true,
rateLimited: true,
cache: DEFAULT_CACHE_STRATEGY,
returns: {} as FullUser,
props: z.object({id: z.string()}).strict(),
summary: 'Get full profile by user ID',
tag: 'Users',
},
// 'user/by-id/:id/lite': {
// method: 'GET',
// authed: false,
// rateLimited: false,
// cache: DEFAULT_CACHE_STRATEGY,
// returns: {} as DisplayUser,
// props: z.object({id: z.string()}).strict(),
// summary: 'Get lightweight profile by user ID',
// },
'user/by-id/:id/block': {
method: 'POST',
authed: true,
rateLimited: false,
props: z.object({id: z.string()}).strict(),
summary: 'Block a user by their ID',
tag: 'Users',
},
'user/by-id/:id/unblock': {
method: 'POST',
authed: true,
rateLimited: false,
props: z.object({id: z.string()}).strict(),
summary: 'Unblock a user by their ID',
tag: 'Users',
},
'ban-user': {
method: 'POST',
@@ -83,9 +129,10 @@ export const API = (_apiTypeCheck = {
unban: z.boolean().optional(),
})
.strict(),
summary: 'Ban or unban a user',
tag: 'Admin',
},
'create-user': {
// TODO rest
method: 'POST',
authed: true,
rateLimited: true,
@@ -96,6 +143,8 @@ export const API = (_apiTypeCheck = {
adminToken: z.string().optional(),
})
.strict(),
summary: 'Create a new user (admin or onboarding flow)',
tag: 'Users',
},
'create-profile': {
method: 'POST',
@@ -103,6 +152,8 @@ export const API = (_apiTypeCheck = {
rateLimited: true,
returns: {} as Row<'profiles'>,
props: baseProfilesSchema,
summary: 'Create a new profile for the authenticated user',
tag: 'Profiles',
},
report: {
method: 'POST',
@@ -119,6 +170,8 @@ export const API = (_apiTypeCheck = {
})
.strict(),
returns: {} as any,
summary: 'Submit a report for content or a user',
tag: 'Moderation',
},
me: {
method: 'GET',
@@ -127,6 +180,8 @@ export const API = (_apiTypeCheck = {
cache: DEFAULT_CACHE_STRATEGY,
props: z.object({}),
returns: {} as FullUser,
summary: 'Get the authenticated user full data',
tag: 'Users',
},
'me/update': {
method: 'POST',
@@ -155,6 +210,8 @@ export const API = (_apiTypeCheck = {
discordHandle: z.string().optional(),
}),
returns: {} as FullUser,
summary: 'Update authenticated user profile and settings',
tag: 'Users',
},
'update-profile': {
method: 'POST',
@@ -162,6 +219,8 @@ export const API = (_apiTypeCheck = {
rateLimited: true,
props: combinedProfileSchema.partial(),
returns: {} as ProfileRow,
summary: 'Update profile fields for the authenticated user',
tag: 'Profiles',
},
'update-notif-settings': {
method: 'POST',
@@ -172,6 +231,8 @@ export const API = (_apiTypeCheck = {
medium: z.enum(['email', 'browser', 'mobile']),
enabled: z.boolean(),
}),
summary: 'Update a notification preference for the user',
tag: 'Notifications',
},
'me/delete': {
method: 'POST',
@@ -180,6 +241,8 @@ export const API = (_apiTypeCheck = {
props: z.object({
username: z.string(), // just so you're sure
}),
summary: 'Delete the authenticated user account',
tag: 'Users',
},
'me/private': {
method: 'GET',
@@ -187,38 +250,8 @@ export const API = (_apiTypeCheck = {
rateLimited: false,
props: z.object({}),
returns: {} as PrivateUser,
},
'user/:username': {
method: 'GET',
authed: false,
rateLimited: false,
cache: DEFAULT_CACHE_STRATEGY,
returns: {} as FullUser,
props: z.object({username: z.string()}).strict(),
},
'user/:username/lite': {
method: 'GET',
authed: false,
rateLimited: false,
cache: DEFAULT_CACHE_STRATEGY,
returns: {} as DisplayUser,
props: z.object({username: z.string()}).strict(),
},
'user/by-id/:id': {
method: 'GET',
authed: false,
rateLimited: false,
cache: DEFAULT_CACHE_STRATEGY,
returns: {} as FullUser,
props: z.object({id: z.string()}).strict(),
},
'user/by-id/:id/lite': {
method: 'GET',
authed: false,
rateLimited: false,
cache: DEFAULT_CACHE_STRATEGY,
returns: {} as DisplayUser,
props: z.object({id: z.string()}).strict(),
summary: 'Get private user data for the authenticated user',
tag: 'Users',
},
'search-users': {
method: 'GET',
@@ -233,6 +266,8 @@ export const API = (_apiTypeCheck = {
page: z.coerce.number().gte(0).default(0),
})
.strict(),
summary: 'Search users by term with pagination',
tag: 'Users',
},
'compatible-profiles': {
method: 'GET',
@@ -246,6 +281,8 @@ export const API = (_apiTypeCheck = {
[userId: string]: CompatibilityScore
}
},
summary: 'Find profiles compatible with a given user',
tag: 'Profiles',
},
'remove-pinned-photo': {
method: 'POST',
@@ -257,6 +294,8 @@ export const API = (_apiTypeCheck = {
userId: z.string(),
})
.strict(),
summary: 'Remove the pinned photo from a profile',
tag: 'Profiles',
},
'get-compatibility-questions': {
method: 'GET',
@@ -270,6 +309,8 @@ export const API = (_apiTypeCheck = {
score: number
})[]
},
summary: 'Retrieve compatibility questions and stats',
tag: 'Compatibility',
},
'like-profile': {
method: 'POST',
@@ -282,6 +323,8 @@ export const API = (_apiTypeCheck = {
returns: {} as {
status: 'success'
},
summary: 'Like or unlike a profile',
tag: 'Profiles',
},
'ship-profiles': {
method: 'POST',
@@ -295,6 +338,8 @@ export const API = (_apiTypeCheck = {
returns: {} as {
status: 'success'
},
summary: 'Create or remove a ship between two profiles',
tag: 'Profiles',
},
'get-likes-and-ships': {
method: 'GET',
@@ -311,6 +356,8 @@ export const API = (_apiTypeCheck = {
likesGiven: LikeData[]
ships: ShipData[]
},
summary: 'Fetch likes and ships for a user',
tag: 'Profiles',
},
'has-free-like': {
method: 'GET',
@@ -321,6 +368,8 @@ export const API = (_apiTypeCheck = {
status: 'success'
hasFreeLike: boolean
},
summary: 'Check whether the user has a free like available',
tag: 'Profiles',
},
'star-profile': {
method: 'POST',
@@ -333,6 +382,8 @@ export const API = (_apiTypeCheck = {
returns: {} as {
status: 'success'
},
summary: 'Star or unstar a profile',
tag: 'Profiles',
},
'get-profiles': {
method: 'GET',
@@ -374,16 +425,8 @@ export const API = (_apiTypeCheck = {
status: 'success' | 'fail'
profiles: Profile[]
},
},
'get-profile-answers': {
method: 'GET',
authed: true,
rateLimited: true,
props: z.object({userId: z.string()}).strict(),
returns: {} as {
status: 'success'
answers: Row<'compatibility_answers'>[]
},
summary: 'List profiles with filters, pagination and ordering',
tag: 'Profiles',
},
'create-comment': {
method: 'POST',
@@ -395,6 +438,8 @@ export const API = (_apiTypeCheck = {
replyToCommentId: z.string().optional(),
}),
returns: {} as any,
summary: 'Create a comment or reply',
tag: 'Profiles',
},
'hide-comment': {
method: 'POST',
@@ -405,6 +450,8 @@ export const API = (_apiTypeCheck = {
hide: z.boolean(),
}),
returns: {} as any,
summary: 'Hide or unhide a comment',
tag: 'Profiles',
},
'get-channel-memberships': {
method: 'GET',
@@ -420,6 +467,8 @@ export const API = (_apiTypeCheck = {
channels: [] as PrivateMessageChannel[],
memberIdsByChannelId: {} as { [channelId: string]: string[] },
},
summary: 'List private message channel memberships',
tag: 'Messages',
},
'get-channel-messages': {
method: 'GET',
@@ -431,6 +480,8 @@ export const API = (_apiTypeCheck = {
id: z.coerce.number().optional(),
}),
returns: [] as PrivateChatMessage[],
summary: 'Retrieve messages for a private channel',
tag: 'Messages',
},
'get-channel-seen-time': {
method: 'GET',
@@ -443,6 +494,8 @@ export const API = (_apiTypeCheck = {
.transform(arrify),
}),
returns: [] as [number, string][],
summary: 'Get last seen times for one or more channels',
tag: 'Messages',
},
'set-channel-seen-time': {
method: 'POST',
@@ -451,12 +504,16 @@ export const API = (_apiTypeCheck = {
props: z.object({
channelId: z.coerce.number(),
}),
summary: 'Set last seen time for a channel',
tag: 'Messages',
},
'set-last-online-time': {
method: 'POST',
authed: true,
rateLimited: false,
props: z.object({}),
summary: 'Update the user last online timestamp',
tag: 'Users',
},
'get-notifications': {
method: 'GET',
@@ -469,6 +526,8 @@ export const API = (_apiTypeCheck = {
limit: z.coerce.number().gte(0).lte(1000).default(100),
})
.strict(),
summary: 'Fetch notifications for the authenticated user',
tag: 'Notifications',
},
'create-private-user-message': {
method: 'POST',
@@ -479,6 +538,8 @@ export const API = (_apiTypeCheck = {
content: contentSchema,
channelId: z.number(),
}),
summary: 'Send a message in a private channel',
tag: 'Messages',
},
'create-private-user-message-channel': {
method: 'POST',
@@ -488,6 +549,8 @@ export const API = (_apiTypeCheck = {
props: z.object({
userIds: z.array(z.string()),
}),
summary: 'Create a new private message channel between users',
tag: 'Messages',
},
'update-private-user-message-channel': {
method: 'POST',
@@ -498,6 +561,8 @@ export const API = (_apiTypeCheck = {
channelId: z.number(),
notifyAfterTime: z.number(),
}),
summary: 'Update settings for a private message channel',
tag: 'Messages',
},
'leave-private-user-message-channel': {
method: 'POST',
@@ -507,6 +572,8 @@ export const API = (_apiTypeCheck = {
props: z.object({
channelId: z.number(),
}),
summary: 'Leave a private message channel',
tag: 'Messages',
},
'create-compatibility-question': {
method: 'POST',
@@ -517,6 +584,8 @@ export const API = (_apiTypeCheck = {
question: z.string().min(1).max(MAX_COMPATIBILITY_QUESTION_LENGTH),
options: z.record(z.string(), z.number()),
}),
summary: 'Create a new compatibility question with options',
tag: 'Compatibility',
},
'set-compatibility-answer': {
method: 'POST',
@@ -532,6 +601,20 @@ export const API = (_apiTypeCheck = {
explanation: z.string().nullable().optional(),
})
.strict(),
summary: 'Submit or update a compatibility answer',
tag: 'Compatibility',
},
'get-profile-answers': {
method: 'GET',
authed: true,
rateLimited: true,
props: z.object({userId: z.string()}).strict(),
returns: {} as {
status: 'success'
answers: Row<'compatibility_answers'>[]
},
summary: 'Get compatibility answers for a profile',
tag: 'Compatibility',
},
'create-vote': {
method: 'POST',
@@ -543,6 +626,8 @@ export const API = (_apiTypeCheck = {
isAnonymous: z.boolean(),
description: contentSchema,
}),
summary: 'Create a new vote/poll',
tag: 'Votes',
},
'vote': {
method: 'POST',
@@ -554,6 +639,8 @@ export const API = (_apiTypeCheck = {
priority: z.number(),
choice: z.enum(['for', 'abstain', 'against']),
}),
summary: 'Cast a vote on an existing poll',
tag: 'Votes',
},
'search-location': {
method: 'POST',
@@ -564,6 +651,8 @@ export const API = (_apiTypeCheck = {
term: z.string(),
limit: z.number().optional(),
}),
summary: 'Search for a location by text',
tag: 'Locations',
},
'search-near-city': {
method: 'POST',
@@ -574,6 +663,8 @@ export const API = (_apiTypeCheck = {
cityId: z.string(),
radius: z.number().min(1).max(500),
}),
summary: 'Find places near a GeoDB city ID within a radius',
tag: 'Locations',
},
'contact': {
method: 'POST',
@@ -584,6 +675,8 @@ export const API = (_apiTypeCheck = {
content: contentSchema,
userId: z.string().optional(),
}),
summary: 'Send a contact/support message',
tag: 'Contact',
},
'get-messages-count': {
method: 'GET',
@@ -591,6 +684,8 @@ export const API = (_apiTypeCheck = {
rateLimited: false,
props: z.object({}),
returns: {} as { count: number },
summary: 'Get the total number of messages (public endpoint)',
tag: 'Messages',
},
'save-subscription': {
method: 'POST',
@@ -600,6 +695,8 @@ export const API = (_apiTypeCheck = {
props: z.object({
subscription: z.record(z.any())
}),
summary: 'Save a push/browser subscription for the user',
tag: 'Notifications',
},
'create-bookmarked-search': {
method: 'POST',
@@ -612,6 +709,8 @@ export const API = (_apiTypeCheck = {
location: z.any().optional(),
search_name: z.string().nullable().optional(),
}),
summary: 'Create a bookmarked search for quick reuse',
tag: 'Searches',
},
'delete-bookmarked-search': {
method: 'POST',
@@ -621,6 +720,8 @@ export const API = (_apiTypeCheck = {
props: z.object({
id: z.number(),
}),
summary: 'Delete a bookmarked search by ID',
tag: 'Searches',
},
} as const)

View File

@@ -8,7 +8,6 @@ import {
DisplayUser,
getDisplayUsers,
getFullUserById,
getUserById,
} from 'web/lib/supabase/users'
import { FullUser } from 'common/api/user-types'
@@ -28,29 +27,29 @@ export function useUserById(userId: string | undefined) {
const cache = new Map<string, DisplayUser | null>()
export function useDisplayUserById(userId: string | undefined) {
const [user, setUser] = usePersistentInMemoryState<
DisplayUser | null | undefined
>(undefined, `user-${userId}`)
useEffect(() => {
if (userId) {
if (cache.has(userId)) {
setUser(cache.get(userId))
} else {
getUserById(userId)
.then((result) => {
cache.set(userId, result)
setUser(result)
})
.catch(() => {
setUser(null)
})
}
}
}, [userId])
return user
}
// export function useDisplayUserById(userId: string | undefined) {
// const [user, setUser] = usePersistentInMemoryState<
// DisplayUser | null | undefined
// >(undefined, `user-${userId}`)
//
// useEffect(() => {
// if (userId) {
// if (cache.has(userId)) {
// setUser(cache.get(userId))
// } else {
// getUserById(userId)
// .then((result) => {
// cache.set(userId, result)
// setUser(result)
// })
// .catch(() => {
// setUser(null)
// })
// }
// }
// }, [userId])
// return user
// }
export function useUsers(userIds: string[]) {
const [users, setUsers] = useState<(DisplayUser | null)[] | undefined>(

View File

@@ -1,7 +1,6 @@
import {db} from './db'
import {run} from 'common/supabase/utils'
import {api} from 'web/lib/api'
import {unauthedApi} from 'common/util/api'
import type {DisplayUser} from 'common/api/user-types'
import {MIN_BIO_LENGTH} from "common/constants";
import {MONTH_MS} from "common/util/time";
@@ -28,20 +27,20 @@ export async function getPrivateUserSafe() {
}
}
export async function getUserById(id: string) {
return unauthedApi('user/by-id/:id/lite', {id})
}
// export async function getUserById(id: string) {
// return unauthedApi('user/by-id/:id/lite', {id})
// }
export async function getUserByUsername(username: string) {
return unauthedApi('user/:username/lite', {username})
}
export async function getFullUserByUsername(username: string) {
return unauthedApi('user/:username', {username})
}
// export async function getUserByUsername(username: string) {
// return unauthedApi('user/:username/lite', {username})
// }
//
// export async function getFullUserByUsername(username: string) {
// return unauthedApi('user/:username', {username})
// }
export async function getFullUserById(id: string) {
return unauthedApi('user/by-id/:id', {id})
return api('user/by-id/:id', {id})
}
export async function searchUsers(prompt: string, limit: number) {