Search code examples
loopbackjsv4l2loopbackloopback4

Route decorator for dynamic routing allowing a slash as part of the varible string


I have a loopback4 controller to fetch and create some files on the server. All files are stored in a directory structure with deeper directories. It might look like this:

├── WA2114
│   ├── 300dpi
│   │   ├── WA2114_Frontal.jpg
│   │   └── WA2114_entnehmen.jpg
│   ├── web
│   │   ├── WA2114-anwendung-Aufbewahrung.jpg
│   │   └── WA2114_Verpackung_NEU.jpg
│   ├── Produktdatenblatt
│   │   └── 2114-000.pdf

I want a method, to fetch all files based on the called RestAPI-Route:

GET /files/{category}

If i call GET /files/WA2114 I want to get a list of all files lying underneath WA2114/. If i call GET /files/WA2114/300dpi i want only the files from the deeper folder ../300dpi. I hope it's clear, what the goal is. The same logic is needed for uploading new files via POST /files/{category}.

I already tried the solutions described here: https://lideo.github.io/loopback.io/doc/en/lb4/Routes.html but with no success.

I have already set up a route for the top directory. But the deeper directories are not reachable dynamically because the route decorator seems to be sticked on the level and slashes are not allowed in the variable. I don't want to create dozens of methods to handle every directory level.

My current controller (simplified):

export class FileController
{
    constructor(@repository(FileRepository) public fileRepository: FileRepository)
    {
    }

    @get('/files/{category}', {
        responses: {
            '200': {
                description: 'Array of File model instances of the specified category',
                content:     {
                    'application/json': {
                        schema: {type: 'array', items: {'x-ts-type': File}}
                    }
                }
            }
        }
    })
    async findbyCategory(@param.path.string('category') category: string): Promise<File[]>
    {
        return await this.fileRepository.findByCategory(category);
    }
}

How do i have to decorate the controller method to be able to dynamically fetch /files/a and files/a/b/c with the same method?

I already did something like this in php/Symphony looking like this: @Route("/files/{category}", methods={"GET"}, requirements={"category"=".+"}). The .+ is the magic here. Now I have to rebuild the code with loopback4 and have to refactor the routings and I'm failing on this one. Someone with a solution for this?


Solution

  • I have already set up a route for the top directory. But the deeper directories are not reachable dynamically because the route decorator seems to be sticked on the level and slashes are not allowed in the variable. I don't want to create dozens of methods to handle every directory level.

    • App will process your request in sequence.ts and find the corresponding router based on your request address and parameters, etc.

    export class MySequence implements SequenceHandler {
        constructor(
            @inject(SequenceActions.FIND_ROUTE) protected findRoute: FindRoute,
            @inject(SequenceActions.PARSE_PARAMS) protected parseParams: ParseParams,
            @inject(SequenceActions.INVOKE_METHOD) protected invoke: InvokeMethod,
            @inject(SequenceActions.SEND) public send: Send,
            @inject(SequenceActions.REJECT) public reject: Reject,
        ) { }
    
        async handle(context: RequestContext) {
            try {
                const { request, response } = context;
                const route = this.findRoute(request); // <= here!!! find router by request
                const args = await this.parseParams(request, route);
                const result = await this.invoke(route, args);
                this.send(response, result);
            } catch (err) {
                this.reject(context, err);
            }
        }
    }
    

    • You need to "cheat" the FindRoute and assign following request to the same router

    Suppose:

    We have a controller A, one of the api is @get("/api/{category}")

    Before:

    /api/dir-1 => url=/api param=dir-1 => controller A

    /api/dir-1/file-2 => url=/api/dir-1 param=file-2 => (cannot find controller)

    /api/dir-1/dir-2/file-3 => url=/api/dir-1/dir-2 param=file-3 => (cannot find controller)

        async handle(context: RequestContext) {
            try {
                const { request, response } = context;
    
                // !
                if (
                    request.method === "GET" && request.url === "/api/"
                ) {
                    request.url = `/files/'${request.url.slice(7).replace(/\//gi, '----')}'`;
                }
    
                const route = this.findRoute(request);
                const args = await this.parseParams(request, route);
                const result = await this.invoke(route, args);
                this.send(response, result);
            } catch (err) {
                this.reject(context, err);
            }
        }
    

    After:

    /api/dir-1 => url=/api param=dir-1 => controller A

    /api/dir-1/file-2 => url=/api param=dir-1----file-2 => controller A

    /api/dir-1/dir-2/file-3 => url=/api param=dir-1----dir-2----file-3 => controller A

    • Finished, now you can handle files in controller A. but I don't think this is a good solution, you need to create a new FindRoute and put the corresponding logic inside.

    Please see my example project for details.