// Copyright (c) 2025 The Brave Authors. All rights reserved. // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this file, // You can obtain one at https://mozilla.org/MPL/2.0/. import { ArgsOfAsyncMethod, AsyncMethodKeys, ResultOfAsyncMethod, } from './api_utils' import { query, QueryEndpointDefinition, MutationEndpointDefinition, PlaceholderQueryEndpointDefinition, NoPlaceholderQueryEndpointDefinition, } from './create_interface_api' // Allow parameters to be the same as for createInterfaceAPI, but generated by the interface we know // as much as possible. type BaseIncomingQuery, Z> = Omit< QueryEndpointDefinition, Z>, 'query' | 'response' > type MutationArgs = T[K] extends (...args: any) => any ? Parameters : never type MutationResult = K extends AsyncMethodKeys ? ResultOfAsyncMethod : void type BaseIncomingMutation< T, K extends keyof T, Z = MutationResult, > = Omit< MutationEndpointDefinition, Z>, 'mutation' | 'response' > type IncomingQueryDefinition< T, K extends AsyncMethodKeys, Z, > = BaseIncomingQuery & { response: (raw: ResultOfAsyncMethod) => Z } type IncomingMutationDefinition< T, K extends keyof T, Z = MutationResult, > = BaseIncomingMutation> & { mutationResponse: (raw: MutationResult) => Z } // Helper: every possible mapper for a key of `T` type EndpointMapper = | IncomingQueryDefinition ? P : never, any> | IncomingMutationDefinition // // Create endpoints generated from some keys of an interface that can be used for createMojoAPI // Call like this: // type MyInterface = { // getConversationEntry: (id: string) => Promise<{ entries: string[] }> // addNewEntry: (entry: string) => Promise<{ id: string, entry: string }> // } // // endpointsFor(conversationHandler as MyInterface, { // getConversationEntry: { // ressponse: (result) => result.entries, // prefetchWithArgs: ['0'], // }, // addNewEntry: { // mutation: (result) => result.screenshots // } // }) // export function endpointsFor< T, const MapperDefinitions extends Partial<{ [P in keyof T]: EndpointMapper }>, >( impl: T, // Intersect `mappers` with a compile-time check so that any mismatch between // `placeholderData` and the return value of `response` triggers an error where the // mapper object is written. mappers: MapperDefinitions & AssertPlaceholderDataMatches & AssertInitialDataMatches, ) { // Keys that actually appear in the supplied mappers object. type MapperKeys = keyof MapperDefinitions type ValidKeys = Extract type ValidIncomingQueryDefinition

= P extends AsyncMethodKeys ? IncomingQueryDefinition : never type ResultEndpoints = { [P in ValidKeys]: MapperDefinitions[P] extends ValidIncomingQueryDefinition< P, infer Z > ? MapperDefinitions[P] extends { placeholderData: any } ? P extends AsyncMethodKeys ? PlaceholderQueryEndpointDefinition, Z> : never : P extends AsyncMethodKeys ? NoPlaceholderQueryEndpointDefinition, Z> : never : MapperDefinitions[P] extends IncomingMutationDefinition ? MutationEndpointDefinition, Z> : never } const result = {} as ResultEndpoints ;(Object.keys(mappers) as Array).forEach((key) => { const mapper = mappers[key]! // non-optional – we just iterated over existing keys if ('response' in mapper) { const endpoint = query({ ...mapper, query: async (...args: any[]) => { const raw = await (impl[key] as (...args: any[]) => Promise)( ...(args as any), ) const mapped = mapper.response(raw) return mapped }, } as any) ;(result as any)[key] = endpoint } else if ('mutationResponse' in mapper) { const endpoint = { ...mapper, mutation: async (...args: any[]) => { let raw: Promise | void = ( impl[key] as (...args: any[]) => Promise | void )(...(args as any)) // Repsonse type of wrapped method is // either Promise or void (undefined). if (raw !== undefined) { raw = await raw } // void is ok if incoming result is void return mapper.mutationResponse(raw as any) }, } as any ;(result as any)[key] = endpoint } }) return result } // If a mapper provides both `response` and `initialData`, ensure that the // supplied `initialData` type is assignable to the type produced by // `response`. We purposely only check the `Init -> R` direction so that // literal-narrower types (e.g. `'ad'` vs `string`) are accepted. // This utility type is a bit crazy but it's a nice-to-have so that the // typescript error is at the correct place when an initialData property // does not have the correct type. type AssertInitialDataMatches = { [K in keyof M]: M[K] extends { response: (...args: any[]) => infer R } ? M[K] extends { initialData?: infer Init } ? Exclude extends never ? {} // nothing to validate : Exclude extends R ? {} // OK: initialData is assignable to R (direct) : Exclude extends Readonly ? {} // OK: initialData is assignable to Readonly (e.g. readonly array) : { __initialDataMismatch__: never } // mismatch: initialData is not the correct type : {} : {} } type AssertPlaceholderDataMatches = { [K in keyof M]: M[K] extends { response: (...args: any[]) => infer R } ? M[K] extends { placeholderData?: infer Init } ? Exclude extends never ? {} // nothing to validate : Exclude extends R ? {} // OK: placeholderData is assignable to R (direct) : Exclude extends Readonly ? {} // OK: placeholderData is assignable to Readonly (e.g. readonly array) : { __placeholderDataMismatch__: never } // mismatch: placeholderData is not the correct type : {} : {} }