Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add dynamic routing #1438

Open
BrunnerLivio opened this issue Jan 12, 2019 · 32 comments
Open

Add dynamic routing #1438

BrunnerLivio opened this issue Jan 12, 2019 · 32 comments
Labels

Comments

@BrunnerLivio
Copy link
Member

BrunnerLivio commented Jan 12, 2019

I'm submitting a...


[ ] Regression 
[ ] Bug report
[x] Feature request
[ ] Documentation issue or request
[ ] Support request => Please do not submit support request here, instead post your question on Stack Overflow.

Current behavior

At the moment there is no way to register a route dynamically except by using the internal HTTP / Fastify / Express instance (related #124):

const expressInstance = express();
expressInstance.use(morgan('dev'));
const app = NestFactory.create(ApplicationModule, expressInstance);
expressInstance.get('/foo', function (req, res) {
    res.send('bar');
})

The problem with this approach is, Nest does internally not recognize this route. Therefore it will not show up in the e.g. swagger integration.

Expected behavior

I wish to have a dynamic router as part of the public Nest API, or at least a way to tell Nest to use an external route inside its router registry.

What is the motivation / use case for changing the behavior?

nestjs/terminus routes do not get registered because it directly modifies the HTTP instance. Therefore there is no Swagger integration or compatibility with middleware.

Related: nest/terminus#32 nest/terminus#33

other integrations such as @zMotivat0r for example uses this rather "hacky" workaround (sorry :P):
https://github.com/nestjsx/crud/blob/e255120b26dd8ca0eae9c7ec9dac4f893051f447/src/crud.module.ts#L5-L15
or nest-router by @shekohex uses what I would consider the internal API:

https://github.com/shekohex/nest-router/blob/3759c688b285792d7889b52f75fffb0e86d4dd54/src/router.module.ts#L53-L56

Environment


Nest version: 5.x.x

 
For Tooling issues:
- Node version: XX  
- Platform:  

Others:

@kamilmysliwiec
Copy link
Member

What exactly would you like to see in the Nest core actually? Do you have any proposals on how the API could potentially look like? Just to clarify, I fully understand the use-case :)

@BrunnerLivio
Copy link
Member Author

BrunnerLivio commented Jan 13, 2019

@kamilmysliwiec Maybe something like this?

const appRoutes: Routes = [
  { 
    path: 'hero',
    method: RequestMethod.POST,
    use: (heroService: HeroService) => heroService.getAll(),
    inject: [HeroService],
    // This option would ignore the `use` option.
    // What it basically does is telling Nest that the 'hero'-path 
    // is registered in the underyling httpServer.
    callThrough: true  
  },
];

@Module({
  imports: [
    RouterModule.forRoot(
      appRoutes,
      {enableTracing: true} // Future possibilities :)
    )
  ],
  ...
})

In general we should thrive for implementing every feature which is accessible through decorators, also a dynamic approach, as shown in this example. As you know, decorators are cool for clean code, but they lack in extensibility (I think you mentioned something similar in a past issue about Java Spring)

@shekohex
Copy link
Contributor

Hi @BrunnerLivio
i would agree with you about the issue of external package sometimes have some sort of * cough cough * poor integration with each others, as an example you mentioned the swagger pkg, btw the first issue in nest-router was Swagger integration nestjsx/nest-router#3 , so your API is nice

const appRoutes: Routes = [
  { 
    path: 'hero',
    method: RequestMethod.POST,
    use: (heroService: HeroService) => heroService.getAll(),
    inject: [HeroService],
    // This option would ignore the `use` option.
    // What it basically does is telling Nest that the 'hero'-path 
    // is registered in the underyling httpServer.
    callThrough: true  
  },
];

@Module({
  imports: [
    RouterModule.forRoot(
      appRoutes,
      {enableTracing: true} // Future possibilities :)
    )
  ],
  ...
})

so do you think we actually need that in the core ?

oh, i think nest router package can do something like that !
it's already have a full access to the module containers, so we can get a ref to any service from any module.
https://github.com/shekohex/nest-router/blob/3759c688b285792d7889b52f75fffb0e86d4dd54/src/router.module.ts#L17

so what do you think ?
but since nest-router was created in first place to solve other problem, nesting routes see nestjsx/nest-router#43 is that possible ?
anyway i could help in either way here in the core or externally in another pkg or in nest-router itself 😄

@BrunnerLivio
Copy link
Member Author

@shekohex thanks a lot for your answer!

so do you think we actually need that in the core ?

I guess it should atleast be at the same place where the routing is handled, therefore nestjs/core I guess?

In my opinion the functionality of your router or similar should be part of the framework. As already hinted at I would like to see every decorator functionality also as non-decorator configurable option. Therefore a public API of the router would be quite handy!

@shekohex
Copy link
Contributor

ok, we can work on that

my opinion the functionality of your router or similar should be part of the framework.

Yeah, that was a long opened issue/discussion in nest-router see nestjsx/nest-router#19 it's about year ago !

i think we can do better, what about that, since most of the users who will use this api most of them will be 3rd party lib creators, we can make it even lower level api, instead of use: (someService: SomeService) => someService.someMethod(),, we give them a reference to the underlying module container itself, or an express-like (req, res, next, container) => {...} function api.

the reason of that, it would make it easier to anyone want to have a full control over the underlying router.
there is a lot of options there, but really i would love the idea of

to see every decorator functionality also as non-decorator configurable option

since i love to use nest in functional manner.

i know i know that nest built around a lot of concepts like DI, IoC, Modules ...etc, but really i love to use nest with just only functions.

@BrunnerLivio
Copy link
Member Author

BrunnerLivio commented Jan 20, 2019

instead of use: (someService: SomeService) => someService.someMethod(),, we give them a reference to the underlying module container itself, or an express-like (req, res, next, container) => {...} function api.

Great input!

New Proposal

Router Module in @nest/core

// Route interfaces
interface RouteRegister {
  path: string;
  method?: RequestMethod;
}

interface RouteUse {
  use: (req: any, res: any, next: Function, container: any) => Promise<any> | any;
}

interface RouteCallThrough {
  callThrough?: boolean;
}

type RouteWithCallThrough = RouteRegister & RouteCallThrough & { callThrough: true };
type RouteWithUse = RouteRegister & RouteUse & { callThrough?: false | undefined | null };

type Route = RouteWithCallThrough | RouteWithUse;

// Module interfaces
interface RouterModuleOptions {
  routes: Route[];
  enableTracing?: boolean;
}

interface RouterOptionsFactory {
  createRouterOptions():
    | Promise<RouterModuleOptions>
    | RouterModuleOptions;
}

interface RouterModuleAsyncOptions
  extends Pick<ModuleMetadata, 'imports'> {
  name?: string;
  useClass?: Type<RouterOptionsFactory>;
  useExisting?: Type<RouterOptionsFactory>;
  useFactory?: (
    ...args: unknown[]
  ) => Promise<RouterModuleOptions> | RouterModuleOptions;
  inject?: unknown[];
}


// Router module implementation
export class RouterModule {
  static forRoot(options: RouterModuleOptions): DynamicModule {
    // Todo: Implement
    return {
      module: RouterModule,
    };
  }
  static forRootAsync(options: RouterModuleAsyncOptions): DynamicModule {
    // Todo: Implement
    return {
      module: RouterModule
    }
  }
}

Usage Synchonous

const routes: Route[] = [
  {
    path: '/underlying-route',
    method: RequestMethod.GET,
    callThrough: true,
  },
  {
    path: '/new-route',
    method: RequestMethod.GET,
    use: (req, res, next, container) => 'test',
  }
];

@Module({
  imports: [
    RouterModule.forRoot({
      routes,
      enableTracing: true
    }),
  ],
})
class AppRoutes { }

Usage Asynchronous

class RouterModuleFactory implements RouterOptionsFactory {
  constructor(private heroController: HeroController) { }
  createRouterOptions(): RouterModuleOptions {
    return {
      routes: [
        {
          path: '/new-route2',
          use: (req, res) => this.heroController.get(req, res),
        }
      ]
    }
  }
}

@Module({
  imports: [
    RouterModule.forRootAsync({
      useClass: RouterModuleFactory,
      inject: [HeroController]
    })
  ],
})
class AppRoutes { }

Disclaimer

The interface Route is kind of complicated. This is due to; if the user uses callThrough: true he mustn't use use. If callThrough: false or undefined the user can use use. Therefore the following would not be possible:

const routes = Route[] = [
  { path: '/test', callThrough: true, use: () => 'test' } // ERROR
]

@kamilmysliwiec @shekohex what do you think? :)

@marcus-sa
Copy link

marcus-sa commented Jan 20, 2019

use doesn't seem descriptive for what it does.
And that's still not a dynamic API as you can only add routes inside decorators.
You should be able to import the container in an injectable and add a route from there as well.

@BrunnerLivio
Copy link
Member Author

BrunnerLivio commented Jan 20, 2019

@marcus-sa thanks for the feedback.

use doesn't seem descriptive for what it does.

I've used use because Express uses app.use too for middleware definitions

And that's still not a dynamic API as you can only add routes inside decorators.
You should be able to import the container in an injectable and add a route from there as well.

I see your point. Maybe an additional RouterService would do the trick? I would still keep the RouterModule though because it is a nice way to define routes at one central point, similar to Angular router. RouterService could then be an exported provider from RouterModule.

RouterModule is still pretty dynamic, because you can import providers using forRootAsync - but in some situations injecting a RouterService is just more convenient. Question is; should we support both? This could maybe raise inconsistencies into a users app if he/she starts using RouterModule, RouterService and @Controller() at the same time.

@marcus-sa
Copy link

marcus-sa commented Jan 20, 2019

@BrunnerLivio yes, but use is an inconsistent name in MAF's (module architecture framework) as by usage it's actually a FactoryProvider, so it should be useFactory instead for naming consistency within the entire Nest framework :)

@BrunnerLivio
Copy link
Member Author

BrunnerLivio commented Jan 20, 2019

@marcus-sa I thought about that too, but it is not actually a factory, its a router handle. On top of that the way I proposed you can not inject anything into the use function, you would have to use the moduleContainer parameter which is given to the use function, or forRootAsync on the RouterModule. So the functionality differs from useFactory. But I agree that use could be misleading, so I am open for a better name. Maybe useHandler or handler?

@marcus-sa
Copy link

marcus-sa commented Jan 21, 2019

@BrunnerLivio yeah I just noticed that too and was about to propose the same useHandler or handler in the beginning

@shekohex
Copy link
Contributor

That's pretty good api, it's actually similar to the nest-router module API.

should we support both? This could maybe raise inconsistencies into a users app if he/she starts using RouterModule, RouterService and @Controller() at the same time.

yup, if we need to make things dynamic and in the same time easy to use, we should also support RouterService.

This could maybe raise inconsistencies...

well, that's a good question here, but i really can't imagine that issue, since we will make nest knows all the routes either they are registered form RouterModule or using @Controller(..), or maybe better, if the user used callThrough for example, nest also should know that we have that route, which is the idea of this issue in 1st place.

I am open for a better name. Maybe useHandler or handler?

useHandler 👍

@BrunnerLivio
Copy link
Member Author

BrunnerLivio commented Jan 21, 2019

@zMotivat0r What are your thoughts on this, since you can benefit with @nestjsx/crud from this too? Any other recommendations from your side before we proceed?

Edit: By the way; I think a RouteService which allows to get and set all routes during runtime could also heavily benefit the codebase of @nestjs/swagger.

@michaelyali
Copy link
Contributor

@BrunnerLivio thanks for asking.
useHandler 👍

What are your thoughts on this, since you can benefit with @nestjsx/crud from this too? Any other recommendations from your side before we proceed?

well, yes. I've started implementing a dynamic module for @nestjsx/crud and it will be much easier to do with Nest dynamic routing. The main point here is to have all decorators functionality also as non-decorator way, including custom route decorators (if not, I want to here why) and pipes.

And for sure, this feature is one of the cool features and this will help to open more possibilities for Nest.

@BrunnerLivio
Copy link
Member Author

I had a private chat with @kamilmysliwiec and I want to keep you sync.

I started implementing the proposal and came across some problems. If you know NestJS behind the scene you realize that the framework has parts which are being provided inside the application context “dependency injection” e.g. HttpModule or outside “facade” e.g. NestApplicationContext.

The current router handler is fully part of the “facade”. The problem is that it is hard to call / modify the “facade” from the “DI”. Therefore we came to the conclusion we need to refactor the router functionality into the application context, so this issue is feasable without any hacky workaround.

We will then import the RouterModule as one of the CoreModule to prevent any breaking changes of the public API.

At the moment both Kamil and I are really busy, so this issue may take some time - except someone else takes on the task :)

@elendirx
Copy link

elendirx commented Mar 23, 2020

I'm not sure about the status of this issue, but this is a really basic framework feature.

Usual API looks like this (router should be injectable provider):

router.addRoute('/login/some-action', LoginController.someAction)
url = router.getUrl(LoginController.someAction, {someParam: 'some value'})

Routes are decoupled from controllers and can be defined in one place. This allows us to keep the app maintainable even with many routes.

Note that in some frameworks (e.g Flask) we'd pass a string 'LoginController.someAction' instead of the method itself, but passing the method allows for code completion and IDE checks.

@kamilmysliwiec
Copy link
Member

Routes are decoupled from controllers and can be defined in one place. This allows us to keep the app maintainable even with many routes.

This is completely contrary to the ideas and principles of Nest. For this, you can use Koa/Express.

@elendirx
Copy link

This is completely contrary to the ideas and principles of Nest

I consider Nest to be the best node framework because of it's built in DI and SRP approach. Could you please elaborate how is separation of responsibility completely contrary to its principles? Because I honestly thought that this kind of decoupling and maintainability is what you aim for.

@1valdis
Copy link

1valdis commented Jul 16, 2020

I just stumbled upon a use case. I have highly modular system and want to prevent a possibility for routes to coincide between modules. So I wanted to pass a "root route" of a controller with module options (when instantiating the module from AppModule), but this doesn't seem to be possible, since route is hardcoded in a @Controller decorator. Please make it possible to be dynamic.

@Areen3
Copy link

Areen3 commented Sep 29, 2020

Hello

My case is also connected with dynamic routing
Currently, my application is generated from metadata prepared in the case. I could for any entity
generate a controller, service etc but I would prefer to redirect multiple routes to one controller.

i.e. dogs, cats, birds go to the CommonControllerForAllCases controller

for now the only solution I have found:
1 in the Get decorator give an array of all paths to be handled by this controller:
@get (['coffees / get', 'dogs / get'])
2 Perform service in the interceptor

Am I right, can this problem be solved better?

@kamilmysliwiec kamilmysliwiec added effort3: weeks priority: low (4) Low-priority issue that needs to be resolved labels Feb 2, 2021
@MiracleWisp
Copy link

Hello!

Is there any updates on this issue?

I am developing an application that proxies photos and resizes them if needed
For example:
/image/test.jpg will return the original image
/image/test.jpg?width=300&height=400 will return a resized image

I decided to make it a module so that other developers can use it in their projects. And I want to give them the ability to configure the url prefix ("image" for now). Right now I can't do it in a pretty way and am forced to use a dirty hack like this https://github.com/nestjsx/crud/blob/e255120b26dd8ca0eae9c7ec9dac4f893051f447/src/crud.module.ts#L5-L15

@jmcdo29
Copy link
Member

jmcdo29 commented Jun 17, 2021

Another option that has been found for changing that path is by making a custom provider that sets the metadata of the class like the following:

{
	provide: Symbol('CONTROLLER_HACK'),
	useFactory: (config: StripeModuleConfig) => {
		const controllerPrefix =
			config.controllerPrefix || 'image';

		Reflect.defineMetadata(
			PATH_METADATA,
			controllerPrefix,
			ControllerForModule
		);
	},
	inject: [MODULE_CONFIG_INJECTOR],
},

@MiracleWisp
Copy link

@jmcdo29 Thanks! This solution looks pretty nice and works great for me.

@faboulaws
Copy link

faboulaws commented Jul 1, 2021

I used this example for adding routes dynamically to a NestJS controller.

It is not extremely flexible but does the job

1. define a factory for your controller

// mymodule.controller.ts
export function getControllerClass({ customPath }): Type<any> {
  @Controller()
  class MyController {
    constructor(private service: MyService) { }

    @Get([customPath])
    async getSomething(@Res() res: Response) {
      //...
    }    
  }
  return MyController
}

in your module

2. configure routes via the module

// mymodule.module.ts
@Module({})
export class MyModule {
  static forRoot(config): DynamicModule {
    const { customPath } = config;
    const MyController = getControllerClass({ customPath })
    return {
      module: MyModule,
      providers: [     
        MyService
      ],
      exports: [MyService],
      controllers: [MyController]
    };
  }
}

@MrMaz
Copy link

MrMaz commented Jan 17, 2022

Another way to override a controller's decorators. This trick uses existing decorator methods, and does not require dynamic classes or mixins.

import { Controller, Post } from '@nestjs/common';
import { MyControllerClass } from './controllers';

// main route
Controller('new/route')(MyControllerClass);

// `doSomething` POST method (possibly sketchy)
Post('sub/route')(
  MyControllerClass,
  'doSomething',
  Object.getOwnPropertyDescriptor(MyControllerClass.prototype, 'doSomething')
);

@Areen3
Copy link

Areen3 commented Jan 19, 2022

I am currently dynamically adding handles (get, post ...) to the handler
using a function
the code lookslike this:
function file:
import { METHOD_METADATA, PATH_METADATA } from '@af-be/common.constants';
import { isString } from '@af-shared/utils';
import { RequestMappingMetadata, RequestMethod } from '@nestjs/common';
import { TMethodDecorator } from '../constants/type';

const defaultMetadata: RequestMappingMetadata = {
path: '/',
method: RequestMethod.GET
};

const requestMapping = (
target: TMethodDecorator,
metadata: RequestMappingMetadata = defaultMetadata
): void => {
const pathMetadata = metadata[PATH_METADATA];
const path = pathMetadata?.length ? pathMetadata : '/';
const requestMethod = metadata[METHOD_METADATA] || RequestMethod.GET;
const currentMetaData: string | Array = Reflect.getMetadata(PATH_METADATA, target);
const pathFromMetaData: string | Array = currentMetaData === undefined ? [] : currentMetaData;
const arrayMetaDataPaths: Array = isString(pathFromMetaData) ? [pathFromMetaData] : pathFromMetaData;
const newArrayPaths: Array = isString(pathMetadata) ? [path] : pathMetadata;
Reflect.defineMetadata(PATH_METADATA, [...arrayMetaDataPaths, ...newArrayPaths], target);
Reflect.defineMetadata(METHOD_METADATA, requestMethod, target);
};

export function MetaDataRequestRegister(method: RequestMethod, target: TMethodDecorator,
path?: string | Array): void {
requestMapping(target, { [PATH_METADATA]: path, [METHOD_METADATA]: method });
}

using:

MetaDataRequestRegister(
RequestMethod.POST,
GetEntityControllersManager.prototype.getEntityHandle,
af/${EProjectTestRouterNames.projecttest}/${EProjectTestServiceName.EProjectTestEntityServiceName}/ +
${EProjectTestEndPointDBNames.GetAssigment}
);

export const HOST_METADATA = 'host';
export const PATH_METADATA = 'path';
export const SCOPE_OPTIONS_METADATA = 'scope:options';
export const METHOD_METADATA = 'method';
export type TMethodDecorator = (...data: Array) => any;

@WilliamPeralta
Copy link

Un altro modo per ignorare i decoratori di un controller. Questo trucco utilizza metodi decoratore esistenti e non richiede classi dinamiche o mixin.

import { Controller, Post } from '@nestjs/common';
import { MyControllerClass } from './controllers';

// main route
Controller('new/route')(MyControllerClass);

// `doSomething` POST method (possibly sketchy)
Post('sub/route')(
  MyControllerClass,
  'doSomething',
  Object.getOwnPropertyDescriptor(MyControllerClass.prototype, 'doSomething')
);

Following this approach it is possible to inject methods on the fly and then decorate them.
I also managed to decorate the parameters to retrieve @query etc etc.

      for (const collectionOperation of Object.keys(
        options.collectionOperations,
      )) {
        const config = options.collectionOperations[collectionOperation];
        ApiController.prototype[collectionOperation] = function (query) {
          return { Ciao: 'Mondo', query };
        };

        Get(config.path)(
          ApiController,
          collectionOperation,
          Object.getOwnPropertyDescriptor(
            ApiController.prototype,
            collectionOperation,
          ),
        );
        Query()(ApiController.prototype, collectionOperation, 0);
      }

      return ApiController;

http://127.0.0.1:4000/api/items/sub/route?ciao=23

{
  "Ciao": "Mondo",
  "query": {
    "ciao": "23"
  }
}

thank you.

@DBain07
Copy link

DBain07 commented Jul 9, 2022

I used this example for adding routes dynamically to a NestJS controller.

It is not extremely flexible but does the job

1. define a factory for your controller

// mymodule.controller.ts
export function getControllerClass({ customPath }): Type<any> {
  @Controller()
  class MyController {
    constructor(private service: MyService) { }

    @Get([customPath])
    async getSomething(@Res() res: Response) {
      //...
    }    
  }
  return MyController
}

in your module

2. configure routes via the module

// mymodule.module.ts
@Module({})
export class MyModule {
  static forRoot(config): DynamicModule {
    const { customPath } = config;
    const MyController = getControllerClass({ customPath })
    return {
      module: MyModule,
      providers: [     
        MyService
      ],
      exports: [MyService],
      controllers: [MyController]
    };
  }
}

Is there anyway we could extend this to dynamically inject services into the controller? I have a module where I dynamically register routes which dynamically create controllers using getControllerClass, but Im wondering if theres anyway I could also pass service methods into the controller such that for given routes, my controller will call certain service methods

@navanjr
Copy link

navanjr commented Nov 22, 2022

Here is my attempt using PATH_METADATA:

// my checkupModule.ts
...
@Module({
  controllers: [QueueController],
  providers: [CheckupService, QueueService],
  exports: [CheckupService, QueueService],
})
export class CheckupModule extends ConfigurableModuleClass {
  static register(options: typeof OPTIONS_TYPE): DynamicModule {
    const { namespace } = options;
    Reflect.defineMetadata(PATH_METADATA, namespace, CheckupController);
    return {
      module: CheckupModule,
      imports: [
        BullModule.registerQueueAsync({
          name: `${namespace}_JOB`,
          inject: [ConfigService],
          useFactory: (configService: ConfigService) => {
            const limiter = limiterConfig(
              configService.get(`${namespace}_LIMITER`),
            );
            return { limiter };
          },
        }),
      ],
      ...super.register(options),
    };
  }
}
hacked PATH_METADATA: RELATIONSHIP
hacked PATH_METADATA: PRODUCT
...
[Nest] 37765  - 11/22/2022, 4:45:35 PM     LOG [RoutesResolver] CheckupController {/PRODUCT}: +0ms
[Nest] 37765  - 11/22/2022, 4:45:35 PM     LOG [RouterExplorer] Mapped {/PRODUCT/status, GET} route +1ms
[Nest] 37765  - 11/22/2022, 4:45:35 PM     LOG [RouterExplorer] Mapped {/PRODUCT/pause, POST} route +0ms
[Nest] 37765  - 11/22/2022, 4:45:35 PM     LOG [RouterExplorer] Mapped {/PRODUCT/resume, POST} route +0ms
...
[Nest] 37765  - 11/22/2022, 4:45:35 PM     LOG [RoutesResolver] CheckupController {/PRODUCT}: +0ms
[Nest] 37765  - 11/22/2022, 4:45:35 PM     LOG [RouterExplorer] Mapped {/PRODUCT/status, GET} route +1ms
[Nest] 37765  - 11/22/2022, 4:45:35 PM     LOG [RouterExplorer] Mapped {/PRODUCT/pause, POST} route +0ms
[Nest] 37765  - 11/22/2022, 4:45:35 PM     LOG [RouterExplorer] Mapped {/PRODUCT/resume, POST} route +0ms

Unfortunately, it appears the router builds out after all modules are registered. Both instances of CheckupController use have the same path.

Is there a way to build out the controllers in each module as they are created?

@fedu
Copy link

fedu commented Feb 1, 2024

Any updates on this? This is hands down the primary reason, why not to choose NestJS as your backend for dynamic integrations.

@nestjs nestjs deleted a comment from marcus-sa Feb 1, 2024
@WilliamPeralta
Copy link

Hi guys, I'm sharing my implementation for generating a dynamic router (controller) in NestJS.

import { Body, Controller, Delete, Get, Param, Patch, Post, Query, Req } from "@nestjs/common";
import { GetCollectionService } from "../services/get.collection.service";
import { GetItemService } from "../services/get.item.service";
import { SaveItemService } from "../services/save.item.service";
import { Calendar } from "../../calendar/models/calendar.entity";

type OperationConfig = {
  entity: any;
  method: 'get' | 'getItem' | 'post' | 'patch' | 'put' | 'delete';
  path?: string;
}

const addMethodsAndDecorate = (controller: any, operations: OperationConfig[]) => {
  for (const config of operations) {

    switch (config.method) {
      case 'get':
        controller.prototype.findAll = function (query) {
          return this.getCollectionService.getCollection({
            entity: config.entity,
            builder: { with: {} },
            filters: [],
            queryArgs: query,
          });
        };
        Get(config.path)(controller, 'findAll', Object.getOwnPropertyDescriptor(controller.prototype, 'findAll'));
        Query()(controller.prototype, 'findAll', 0);
        break;
      case 'getItem':
        controller.prototype.findOne = async function (id: string) {
          return this.getItemService.getItem(id, { entity: config.entity });
        };
        Get(':id')(controller, 'findOne', Object.getOwnPropertyDescriptor(controller.prototype, 'findOne'));
        Param('id')(controller.prototype, 'findOne', 0);
        break;
      case 'post':
        controller.prototype.create = function (body) {
          const payload = {
            ...(body ?? {}),
            tenant: { id: 2 },
          };
          return this.saveItemService.save(payload, { entity: config.entity });
        };
        Post(config.path)(controller, 'create', Object.getOwnPropertyDescriptor(controller.prototype, 'create'));
        Body()(controller.prototype, 'create', 0);
        break;
      case 'patch':
        controller.prototype.partialUpdate = async function (id: string, req) {
          const item = await this.getItemService.getItem(id, { entity: config.entity });
          if (!item) {
            throw new Error('Not found');
          }
          const payload = req.body;
          return this.saveItemService.save({ id: item.id, ...payload }, { entity: config.entity });
        };
        Patch(":id")(controller, 'partialUpdate', Object.getOwnPropertyDescriptor(controller.prototype, 'partialUpdate'));
        Param('id')(controller.prototype, 'partialUpdate', 0);
        Req()(controller.prototype, 'partialUpdate', 1);
        break;
      case 'delete':
        controller.prototype.deleteOne = async function (id: string) {
          const item = await this.getItemService.getItem(id, { entity: config.entity });
          if (!item) {
            throw new Error('Not found');
          }
          return this.userCreateService.delete(item);
        };
        Delete(':id')(controller, 'deleteOne', Object.getOwnPropertyDescriptor(controller.prototype, 'deleteOne'));
        Param('id')(controller.prototype, 'deleteOne', 0);
        break;
    }
  }

  return controller;
}

const crudFactory = (namespace: string, operations: OperationConfig[]) => {
  @Controller(namespace)
  class CalendarController {
    constructor(
      public getCollectionService: GetCollectionService,
      public getItemService: GetItemService,
      public saveItemService: SaveItemService,
    ) {}
  }
  return addMethodsAndDecorate(CalendarController, operations);
}

export const CrudController = crudFactory('api/cals', [
  {
    method: 'get',
    entity: Calendar,
  },
  {
    method: 'post',
    entity: Calendar,
  },
  {
    method: 'patch',
    entity: Calendar,
  },
  {
    method: 'getItem',
    entity: Calendar,
  },
  {
    method: 'delete',
    entity: Calendar,
  }
])

// You can also take the generated class and extend it to add more methods.

export class MyNewController extends CrudController {
  constructor(
    public getCollectionService: GetCollectionService,
    public getItemService: GetItemService,
    public saveItemService: SaveItemService,
  ) {
    super(getCollectionService, getItemService, saveItemService);
  }

  // ...add more methods
}

@danielsharvey
Copy link

I've been looking at Lazy loading modules which works well with some of the discussion above.

It does not allow Controller routes to be added, something I'm interested in and looking at. Has anybody else looked at this?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests