@@ -20,7 +20,6 @@ import {getLikesAndShips} from './get-likes-and-ships'
2020import { getProfileAnswers } from './get-profile-answers'
2121import { getProfiles } from './get-profiles'
2222import { getSupabaseToken } from './get-supabase-token'
23- import { getDisplayUser , getUser } from './get-user'
2423import { getMe } from './get-me'
2524import { hasFreeLike } from './has-free-like'
2625import { health } from './health'
@@ -53,7 +52,6 @@ import {getNotifications} from './get-notifications'
5352import { updateNotifSettings } from './update-notif-setting'
5453import { setLastOnlineTime } from './set-last-online-time'
5554import swaggerUi from "swagger-ui-express"
56- import * as fs from "fs"
5755import { sendSearchNotifications } from "api/send-search-notifications" ;
5856import { sendDiscordMessage } from "common/discord/core" ;
5957import { getMessagesCount } from "api/get-messages-count" ;
@@ -63,6 +61,10 @@ import {contact} from "api/contact";
6361import { saveSubscription } from "api/save-subscription" ;
6462import { createBookmarkedSearch } from './create-bookmarked-search'
6563import { deleteBookmarkedSearch } from './delete-bookmarked-search'
64+ import { OpenAPIV3 } from 'openapi-types' ;
65+ import { version as pkgVersion } from './../package.json'
66+ import { z , ZodFirstPartyTypeKind , ZodTypeAny } from "zod" ;
67+ import { getUser } from "api/get-user" ;
6668
6769// const corsOptions: CorsOptions = {
6870// origin: ['*'], // Only allow requests from this domain
@@ -117,17 +119,182 @@ const apiErrorHandler: ErrorRequestHandler = (error, _req, res, _next) => {
117119export const app = express ( )
118120app . use ( requestMonitoring )
119121
120- const swaggerDocument = JSON . parse ( fs . readFileSync ( "./openapi.json" , "utf-8" ) )
121- swaggerDocument . info = {
122- ...swaggerDocument . info ,
123- description : "Compass is a free, open-source platform to help people form deep, meaningful, and lasting connections — whether platonic, romantic, or collaborative. It’s 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." ,
124- version : "1.0.0" ,
125- contact : {
126- name : "Compass" ,
127- 128- url : "https://compassmeet.com"
122+ const schemaCache = new WeakMap < ZodTypeAny , any > ( ) ;
123+
124+ export function zodToOpenApiSchema (
125+ zodObj : ZodTypeAny ,
126+ nameHint ?: string
127+ ) : any { // Prevent infinite recursion
128+ if ( schemaCache . has ( zodObj ) ) {
129+ return schemaCache . get ( zodObj ) ;
129130 }
130- } ;
131+
132+ const def : any = ( zodObj as any ) . _def ;
133+ const typeName = def . typeName as ZodFirstPartyTypeKind ;
134+
135+ // Placeholder so recursive references can point here
136+ const placeholder : any = { } ;
137+ schemaCache . set ( zodObj , placeholder ) ;
138+
139+ let schema : any ;
140+
141+ switch ( typeName ) {
142+ case 'ZodString' :
143+ schema = { type : 'string' } ;
144+ break ;
145+ case 'ZodNumber' :
146+ schema = { type : 'number' } ;
147+ break ;
148+ case 'ZodBoolean' :
149+ schema = { type : 'boolean' } ;
150+ break ;
151+ case 'ZodEnum' :
152+ schema = { type : 'string' , enum : def . values } ;
153+ break ;
154+ case 'ZodArray' :
155+ schema = { type : 'array' , items : zodToOpenApiSchema ( def . type ) } ;
156+ break ;
157+ case 'ZodObject' : {
158+ const shape = def . shape ( ) ;
159+ const properties : Record < string , any > = { } ;
160+ const required : string [ ] = [ ] ;
161+
162+ for ( const key in shape ) {
163+ const child = shape [ key ] ;
164+ properties [ key ] = zodToOpenApiSchema ( child , key ) ;
165+ if ( ! child . isOptional ( ) ) required . push ( key ) ;
166+ }
167+
168+ schema = {
169+ type : 'object' ,
170+ properties,
171+ ...( required . length ? { required } : { } ) ,
172+ } ;
173+ break ;
174+ }
175+ case 'ZodRecord' :
176+ schema = {
177+ type : 'object' ,
178+ additionalProperties : zodToOpenApiSchema ( def . valueType ) ,
179+ } ;
180+ break ;
181+ case 'ZodIntersection' : {
182+ const left = zodToOpenApiSchema ( def . left ) ;
183+ const right = zodToOpenApiSchema ( def . right ) ;
184+ schema = { allOf : [ left , right ] } ;
185+ break ;
186+ }
187+ case 'ZodLazy' :
188+ // Recursive schema: use a $ref placeholder name
189+ schema = {
190+ $ref : `#/components/schemas/${ nameHint ?? 'RecursiveType' } ` ,
191+ } ;
192+ break ;
193+ case 'ZodUnion' :
194+ schema = {
195+ oneOf : def . options . map ( ( opt : ZodTypeAny ) => zodToOpenApiSchema ( opt ) ) ,
196+ } ;
197+ break ;
198+ default :
199+ schema = { type : 'string' } ; // fallback for unhandled
200+ }
201+
202+ Object . assign ( placeholder , schema ) ;
203+ return schema ;
204+ }
205+
206+ function generateSwaggerPaths ( api : typeof API ) {
207+ const paths : Record < string , any > = { } ;
208+
209+ for ( const [ route , config ] of Object . entries ( api ) ) {
210+ const pathKey = '/' + route . replace ( / _ / g, '-' ) ; // optional: convert underscores to dashes
211+ const method = config . method . toLowerCase ( ) ;
212+ const summary = ( config as any ) . summary ?? route ;
213+
214+ // Include props in request body for POST/PUT
215+ const operation : any = {
216+ summary,
217+ tags : [ ( config as any ) . tag ?? 'API' ] ,
218+ responses : {
219+ 200 : {
220+ description : 'OK' ,
221+ content : {
222+ 'application/json' : {
223+ schema : { type : 'object' } , // could be improved by introspecting returns
224+ } ,
225+ } ,
226+ } ,
227+ } ,
228+ } ;
229+
230+ // Include props in request body for POST/PUT
231+ if ( config . props && [ 'post' , 'put' , 'patch' ] . includes ( method ) ) {
232+ operation . requestBody = {
233+ required : true ,
234+ content : {
235+ 'application/json' : {
236+ schema : zodToOpenApiSchema ( config . props ) ,
237+ } ,
238+ } ,
239+ } ;
240+ }
241+
242+ // Include props as query parameters for GET/DELETE
243+ if ( config . props && [ 'get' , 'delete' ] . includes ( method ) ) {
244+ const shape = ( config . props as z . ZodObject < any > ) . _def . shape ( ) ;
245+ operation . parameters = Object . entries ( shape ) . map ( ( [ key , zodType ] ) => {
246+ const typeMap : Record < string , string > = {
247+ ZodString : 'string' ,
248+ ZodNumber : 'number' ,
249+ ZodBoolean : 'boolean' ,
250+ } ;
251+ const t = zodType as z . ZodTypeAny ; // assert type to ZodTypeAny
252+ return {
253+ name : key ,
254+ in : 'query' ,
255+ required : ! ( t . isOptional ?? false ) ,
256+ schema : { type : typeMap [ t . _def . typeName ] ?? 'string' } ,
257+ } ;
258+ } ) ;
259+ }
260+
261+ paths [ pathKey ] = {
262+ [ method ] : operation ,
263+ }
264+
265+ if ( config . authed ) {
266+ operation . security = [ { BearerAuth : [ ] } ] ;
267+ }
268+ }
269+
270+ return paths ;
271+ }
272+
273+
274+ const swaggerDocument : OpenAPIV3 . Document = {
275+ openapi : "3.0.0" ,
276+ info : {
277+ title : "Compass API" ,
278+ description : "Compass is a free, open-source platform to help people form deep, meaningful, and lasting connections — whether platonic, romantic, or collaborative. It’s 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." ,
279+ version : pkgVersion ,
280+ contact : {
281+ name : "Compass" ,
282+ 283+ url : "https://compassmeet.com"
284+ }
285+ } ,
286+ paths : generateSwaggerPaths ( API ) ,
287+ components : {
288+ securitySchemes : {
289+ BearerAuth : {
290+ type : 'http' ,
291+ scheme : 'bearer' ,
292+ bearerFormat : 'JWT' ,
293+ } ,
294+ } ,
295+ }
296+ } as OpenAPIV3 . Document ;
297+
131298
132299const rootPath = pathWithPrefix ( "/" )
133300app . get ( rootPath , swaggerUi . setup ( swaggerDocument ) )
@@ -142,10 +309,10 @@ const handlers: { [k in APIPath]: APIHandler<k> } = {
142309 'get-supabase-token' : getSupabaseToken ,
143310 'get-notifications' : getNotifications ,
144311 'mark-all-notifs-read' : markAllNotifsRead ,
145- 'user/:username' : getUser ,
146- 'user/:username/lite' : getDisplayUser ,
312+ // 'user/:username': getUser,
313+ // 'user/:username/lite': getDisplayUser,
147314 'user/by-id/:id' : getUser ,
148- 'user/by-id/:id/lite' : getDisplayUser ,
315+ // 'user/by-id/:id/lite': getDisplayUser,
149316 'user/by-id/:id/block' : blockUser ,
150317 'user/by-id/:id/unblock' : unblockUser ,
151318 'search-users' : searchUsers ,
@@ -218,8 +385,6 @@ Object.entries(handlers).forEach(([path, handler]) => {
218385 }
219386} )
220387
221- // console.debug('COMPASS_API_KEY:', process.env.COMPASS_API_KEY)
222-
223388// Internal Endpoints
224389app . post ( pathWithPrefix ( "/internal/send-search-notifications" ) ,
225390 async ( req , res ) => {
0 commit comments