Browse Source

Update operations

Turn operation class into a builder.
refactor/new-arch
TheoryOfNekomata 6 months ago
parent
commit
528a3a47c0
7 changed files with 75 additions and 19 deletions
  1. +1
    -1
      packages/core/src/client/index.ts
  2. +1
    -0
      packages/core/src/common/endpoint.ts
  3. +35
    -0
      packages/core/src/common/operation.ts
  4. +2
    -2
      packages/core/src/common/recipe.ts
  5. +5
    -2
      packages/core/src/extenders/http/backend/core.ts
  6. +15
    -3
      packages/core/src/extenders/http/client.ts
  7. +16
    -11
      packages/core/test/http/default.test.ts

+ 1
- 1
packages/core/src/client/index.ts View File

@@ -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>;
}

+ 1
- 0
packages/core/src/common/endpoint.ts View File

@@ -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);



+ 35
- 0
packages/core/src/common/operation.ts View File

@@ -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>(


+ 2
- 2
packages/core/src/common/recipe.ts View File

@@ -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(


+ 5
- 2
packages/core/src/extenders/http/backend/core.ts View File

@@ -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


+ 15
- 3
packages/core/src/extenders/http/client.ts View File

@@ -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
},
);
}


+ 16
- 11
packages/core/test/http/default.test.ts View File

@@ -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);



Loading…
Cancel
Save