Tak jak wspomniałem w poprzednim poście, pewne braki w API RPC skłoniły mnie do stworzenia własnego BE. Mógłbym pewnie napisać go znacznie szybciej, gdyby nie to, że chciałbym go wykorzystać w przyszłości do kolejnych serwisów nie modyfikując go zbytnio. Rdzeniem aplikacji będzie plik konfiguracyjny, który będzie elastyczny i pozwoli realizować różne funkcjonalności, chociaż domyślam się, że jakieś bardziej skomplikowane rzeczy będą musiały być implementowane w TS.
Ostatnim razem zaimplementowałem obsługę JSONata do parsowania bloków przychodzących, a następnie wsadzanie tych danych prosto do bazy. Tym razem zacząłem implementację pobierania danych z bazy. Oczywiście zdecydowałem się wcześniej na użycie Nest.JS i swaggera aby dało się w łatwy sposób sprawdzić konfigurację z poziomu strony. No i już na start pojawił się mały problem. Dekoratory, które są używane przez swaggera modyfikują odpowiednio metadane klasy tak aby potem w odpowiedni sposób pokazać wszystko na stronie. Wymyśliłem, że stworzę swój dekorator, który będzie wołał kolejne dekoratory swaggera i nest.js z parametrami, które są przekazywane przez plik konfiguracyjny.
const endpoints = config.get('api') ?? {};
function ConfigEndpoints(name: string) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor,
) {
const endpoint = endpoints[name];
const originalMethod = descriptor.value;
for (const path in endpoint) {
// Add a new method to the class
const func = function (type: string, ...args: any[]) {
console.log(type, ...args);
// Call the original method with the first argument set to the method name
return originalMethod.apply(this, [path, type, ...args]);
};
const desc = {
value: func,
writable: false,
enumerable: false,
configurable: false,
};
Object.defineProperty(target, `${name}_${path}`, desc);
// Decorate the original method with @Get
Get(`/${path}`)(target, `${name}_${path}`, desc);
const settings = endpoint[path];
for (const param in settings.params) {
switch (settings.params[param]) {
// FIXME: Can be more sexy
case 'number':
Bind(Param(param))(target, `${name}_${path}`, desc);
ApiParam({ name: param, type: Number })(
target,
`${name}_${path}`,
desc,
);
break;
case 'string':
default:
Bind(Param(param))(target, `${name}_${path}`, desc);
ApiParam({ name: param, type: String })(
target,
`${name}_${path}`,
desc,
);
}
}
}
return descriptor;
};
}
@Controller('cherpilo')
export class CherpiloController {
constructor(private readonly service: CherpiloService) {}
@ConfigEndpoints('posts')
posts(path: string, type: string, ...args: any[]): boolean {
return this.service.posts(path, type, ...args);
}
}
Dzięki temu taka konfiguracja:
api:
posts:
posts/tag/:name:
params:
name: string
posts/user/:name:
params:
name: string
zamienia się w coś tego typu:
No nic teraz zostało jeszcze stworzenie jakiejś dynamicznej projekcji, która z bazy wyciągnie odpowiednie dane.