From 528a3a47c040cd3012af0a733a4fb9cf43abd1f3 Mon Sep 17 00:00:00 2001 From: TheoryOfNekomata Date: Fri, 31 May 2024 14:52:21 +0800 Subject: [PATCH] Update operations Turn operation class into a builder. --- packages/core/src/client/index.ts | 2 +- packages/core/src/common/endpoint.ts | 1 + packages/core/src/common/operation.ts | 35 +++++++++++++++++++ packages/core/src/common/recipe.ts | 4 +-- .../core/src/extenders/http/backend/core.ts | 7 ++-- packages/core/src/extenders/http/client.ts | 18 ++++++++-- packages/core/test/http/default.test.ts | 27 ++++++++------ 7 files changed, 75 insertions(+), 19 deletions(-) diff --git a/packages/core/src/client/index.ts b/packages/core/src/client/index.ts index fd1c211..fa00fa2 100644 --- a/packages/core/src/client/index.ts +++ b/packages/core/src/client/index.ts @@ -12,5 +12,5 @@ export interface Client; disconnect(connection?: Connection): Promise; at(endpoint: TheEndpoint, params?: Record, unknown>): this; - makeRequest(operation: Operation, query?: URLSearchParams): ReturnType; + makeRequest(operation: Operation): ReturnType; } diff --git a/packages/core/src/common/endpoint.ts b/packages/core/src/common/endpoint.ts index 485e6c4..2d439fc 100644 --- a/packages/core/src/common/endpoint.ts +++ b/packages/core/src/common/endpoint.ts @@ -22,6 +22,7 @@ export const serializeEndpointQueue = (endpointQueue: EndpointQueue) => { }; export const parseToEndpointQueue = (urlWithoutBase: string, endpoints: NamedSet) => { + // 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); diff --git a/packages/core/src/common/operation.ts b/packages/core/src/common/operation.ts index d875161..f4969de 100644 --- a/packages/core/src/common/operation.ts +++ b/packages/core/src/common/operation.ts @@ -27,16 +27,51 @@ export interface BaseOperationParams< export interface Operation { name: Params['name']; method: Params['method']; + searchParams?: URLSearchParams; + search: (...args: ConstructorParameters) => Operation; + setBody: (b: unknown) => Operation; + body?: unknown; } class OperationInstance implements Operation { 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): Operation { + 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 { + switch (this.method) { + case 'PATCH': + case 'PUT': + case 'POST': + case 'QUERY': + this.theBody = b; + break; + default: + break; + } + + return this; + } } export const operation = ( diff --git a/packages/core/src/common/recipe.ts b/packages/core/src/common/recipe.ts index a591cf9..27bff2f 100644 --- a/packages/core/src/common/recipe.ts +++ b/packages/core/src/common/recipe.ts @@ -3,7 +3,7 @@ import {Operation} from './operation'; import {App} from './app'; import {Endpoint} from './endpoint'; -export interface RecipeState { +export interface RecipeState = App> { app: A; backend?: Backend; dataSource?: DataSource; @@ -11,7 +11,7 @@ export interface RecipeState { endpoints?: Record; } -export type Recipe = (a: RecipeState) => RecipeState; +export type Recipe = App, B extends A = A> = (a: RecipeState) => RecipeState; export const composeRecipes = (recipes: Recipe[]): Recipe => (params) => ( recipes.reduce( diff --git a/packages/core/src/extenders/http/backend/core.ts b/packages/core/src/extenders/http/backend/core.ts index 7fc733a..3bb2ee5 100644 --- a/packages/core/src/extenders/http/backend/core.ts +++ b/packages/core/src/extenders/http/backend/core.ts @@ -36,6 +36,8 @@ class ServerInstance implements Server { 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 implements Server { 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 implements Server { return; } + // TODO serialize using content-negotiation params const bodyToSerialize = responseSpec.body; res.statusMessage = responseSpec.statusMessage; // TODO add default status message per status code diff --git a/packages/core/src/extenders/http/client.ts b/packages/core/src/extenders/http/client.ts index d052d1e..4e1d4b5 100644 --- a/packages/core/src/extenders/http/client.ts +++ b/packages/core/src/extenders/http/client.ts @@ -50,7 +50,7 @@ class ClientInstance implements Client { 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 implements Client { 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 }, ); } diff --git a/packages/core/test/http/default.test.ts b/packages/core/test/http/default.test.ts index 74514c3..366d787 100644 --- a/packages/core/test/http/default.test.ts +++ b/packages/core/test/http/default.test.ts @@ -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);