@@ -12,5 +12,5 @@ export interface Client<App extends BaseApp = BaseApp, Connection extends Client | |||
connect(params: ServiceParams): Promise<Connection>; | |||
disconnect(connection?: Connection): Promise<void>; | |||
at<TheEndpoint extends Endpoint = Endpoint>(endpoint: TheEndpoint, params?: Record<GetEndpointParams<TheEndpoint>, unknown>): this; | |||
makeRequest(operation: Operation, query?: URLSearchParams): ReturnType<typeof fetch>; | |||
makeRequest(operation: Operation): ReturnType<typeof fetch>; | |||
} |
@@ -22,6 +22,7 @@ export const serializeEndpointQueue = (endpointQueue: EndpointQueue) => { | |||
}; | |||
export const parseToEndpointQueue = (urlWithoutBase: string, endpoints: NamedSet<Endpoint>) => { | |||
// why we don't get the url without query params as parameter, because we might need the query params in the future | |||
const [urlWithoutQueryParams] = urlWithoutBase.split('?'); | |||
const fragments = urlWithoutQueryParams.split('/').filter((s) => s.trim().length > 0); | |||
@@ -27,16 +27,51 @@ export interface BaseOperationParams< | |||
export interface Operation<Params extends BaseOperationParams = BaseOperationParams> { | |||
name: Params['name']; | |||
method: Params['method']; | |||
searchParams?: URLSearchParams; | |||
search: (...args: ConstructorParameters<typeof URLSearchParams>) => Operation<Params>; | |||
setBody: (b: unknown) => Operation<Params>; | |||
body?: unknown; | |||
} | |||
class OperationInstance<Params extends BaseOperationParams = BaseOperationParams> implements Operation<Params> { | |||
readonly name: Params['name']; | |||
readonly method: Params['method']; | |||
theSearchParams?: URLSearchParams; | |||
// todo add type safety, depend on method when allowing to have body | |||
theBody?: unknown; | |||
constructor(params: Params) { | |||
this.name = params.name; | |||
this.method = params.method ?? 'GET'; | |||
} | |||
search(...args: ConstructorParameters<typeof URLSearchParams>): Operation<Params> { | |||
this.theSearchParams = new URLSearchParams(...args); | |||
return this; | |||
} | |||
get searchParams() { | |||
return Object.freeze(this.theSearchParams); | |||
} | |||
get body() { | |||
return Object.freeze(this.theBody); | |||
} | |||
setBody(b: unknown): Operation<Params> { | |||
switch (this.method) { | |||
case 'PATCH': | |||
case 'PUT': | |||
case 'POST': | |||
case 'QUERY': | |||
this.theBody = b; | |||
break; | |||
default: | |||
break; | |||
} | |||
return this; | |||
} | |||
} | |||
export const operation = <Params extends BaseOperationParams = BaseOperationParams>( | |||
@@ -3,7 +3,7 @@ import {Operation} from './operation'; | |||
import {App} from './app'; | |||
import {Endpoint} from './endpoint'; | |||
export interface RecipeState<A extends App = App> { | |||
export interface RecipeState<AppName extends string = string, A extends App<AppName> = App<AppName>> { | |||
app: A; | |||
backend?: Backend<A>; | |||
dataSource?: DataSource; | |||
@@ -11,7 +11,7 @@ export interface RecipeState<A extends App = App> { | |||
endpoints?: Record<string, Endpoint>; | |||
} | |||
export type Recipe<A extends App = App, B extends A = A> = (a: RecipeState<A>) => RecipeState<B>; | |||
export type Recipe<SA extends string = string, A extends App<SA> = App<SA>, B extends A = A> = (a: RecipeState<SA, A>) => RecipeState<SA, B>; | |||
export const composeRecipes = (recipes: Recipe[]): Recipe => (params) => ( | |||
recipes.reduce( | |||
@@ -36,6 +36,8 @@ class ServerInstance<Backend extends BaseBackend> implements Server<Backend> { | |||
return; | |||
} | |||
console.log(req.method, req.url); | |||
const endpoints = parseToEndpointQueue(req.url, this.backend.app.endpoints); | |||
const [endpoint, endpointParams] = endpoints.at(-1) ?? []; | |||
@@ -51,8 +53,8 @@ class ServerInstance<Backend extends BaseBackend> implements Server<Backend> { | |||
return; | |||
} | |||
const endpointOperations = Array.from(endpoint?.operations ?? []); | |||
if (!endpointOperations.includes(foundAppOperation.name)) { | |||
if (!endpoint?.operations?.has(foundAppOperation.name)) { | |||
const endpointOperations = Array.from(endpoint?.operations ?? []); | |||
res.writeHead(statusCodes.HTTP_STATUS_METHOD_NOT_ALLOWED, { | |||
'Allow': endpointOperations | |||
.map((a) => appOperations.find((aa) => aa.name === a)?.method) | |||
@@ -94,6 +96,7 @@ class ServerInstance<Backend extends BaseBackend> implements Server<Backend> { | |||
return; | |||
} | |||
// TODO serialize using content-negotiation params | |||
const bodyToSerialize = responseSpec.body; | |||
res.statusMessage = responseSpec.statusMessage; // TODO add default status message per status code | |||
@@ -50,7 +50,7 @@ class ClientInstance<App extends BaseApp> implements Client<App> { | |||
return this; | |||
} | |||
makeRequest(operation: Operation, query?: URLSearchParams) { | |||
makeRequest(operation: Operation) { | |||
const baseUrlFragments = [ | |||
this.connection?.host ?? '0.0.0.0' | |||
]; | |||
@@ -69,18 +69,30 @@ class ClientInstance<App extends BaseApp> implements Client<App> { | |||
this.connection?.basePath ? `${this.connection.basePath}${urlString}` : urlString, | |||
`${scheme}://${baseUrlFragments.join(':')}` | |||
); | |||
if (typeof query !== 'undefined') { | |||
url.search = query.toString(); | |||
if (typeof operation.searchParams !== 'undefined') { | |||
url.search = operation.searchParams.toString(); | |||
} | |||
const rawEffectiveMethod = (operation.method ?? 'GET').toUpperCase(); | |||
const finalEffectiveMethod = (AVAILABLE_EXTENSION_METHODS as unknown as string[]).includes(rawEffectiveMethod) | |||
? 'POST' as const | |||
: rawEffectiveMethod; | |||
if (typeof operation.body !== 'undefined') { | |||
return this.fetchFn( | |||
url, | |||
{ | |||
method: finalEffectiveMethod, | |||
body: operation.body as string, | |||
// TODO inject headers | |||
}, | |||
); | |||
} | |||
return this.fetchFn( | |||
url, | |||
{ | |||
method: finalEffectiveMethod, | |||
// TODO inject headers | |||
}, | |||
); | |||
} | |||
@@ -46,10 +46,6 @@ describe('default', () => { | |||
}); | |||
beforeAll(async () => { | |||
const theRawApp = app({ | |||
name: 'default' as const, | |||
}); | |||
const { | |||
app: theApp, | |||
operations, | |||
@@ -58,7 +54,9 @@ describe('default', () => { | |||
addResourceRecipe({ endpointName: 'users', dataSource, }), | |||
addResourceRecipe({ endpointName: 'posts', dataSource, }) | |||
])({ | |||
app: theRawApp, | |||
app: app({ | |||
name: 'default' as const, | |||
}), | |||
}); | |||
theRawEndpoint = theApp.endpoints.get('users'); | |||
@@ -95,9 +93,12 @@ describe('default', () => { | |||
// object so as the client is not limited to .text(), .json(), .arrayBuffer() etc) | |||
const responseRaw = await theClient | |||
.at(theRawEndpoint) | |||
.makeRequest(theOperation, new URLSearchParams({ | |||
foo: 'bar', | |||
})); | |||
.makeRequest( | |||
theOperation | |||
.search({ | |||
foo: 'bar', | |||
}) | |||
); | |||
const response = ResourceItemFetchedResponse.fromFetchResponse(responseRaw); | |||
@@ -114,9 +115,13 @@ describe('default', () => { | |||
// object so as the client is not limited to .text(), .json(), .arrayBuffer() etc) | |||
const responseRaw = await theClient | |||
.at(theRawEndpoint, { resourceId: 3 }) | |||
.makeRequest(theOperation, new URLSearchParams({ | |||
foo: 'bar', | |||
})); | |||
// TODO how to inject extra data (e.g. headers, body) in the operation (e.g. auth)? | |||
.makeRequest( | |||
theOperation | |||
.search({ | |||
foo: 'bar', | |||
}) // allow multiple calls of .search() to add to search params | |||
); | |||
const response = ResourceItemFetchedResponse.fromFetchResponse(responseRaw); | |||