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

Fix dynamic feature loading #230

Merged
merged 2 commits into from
Jun 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 8 additions & 18 deletions src/lib/infrastructure/ioc/container-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,9 @@ import SwitchAccountInputPort from "@/lib/core/port/primary/switch-account-input
import SwitchAccountUseCase from "@/lib/core/use-case/switch-account-usecase";
import SwitchAccountController, { ISwitchAccountController } from "../controller/switch-account-controller";
import SwitchAccountPresenter from "../presenter/switch-account-presenter";
import { ListDIDsInputPort } from "@/lib/core/port/primary/list-dids-ports";
import ListDIDsUseCase from "@/lib/core/use-case/list-dids-usecase";
import ListDIDsController, { ListDIDsControllerParameters } from "../controller/list-dids-controller";
import ListDIDsPresenter from "../presenter/list-dids-presenter";
import { ListDIDsRequest } from "@/lib/core/usecase-models/list-dids-usecase-models";
import { BaseController } from "@/lib/sdk/controller";
import { loadFeatures } from "@/lib/sdk/ioc-helpers";
import { loadFeatures, loadFeaturesSync } from "@/lib/sdk/ioc-helpers";
import ListDidsFeature from "./features/list-dids-feature";
import LoginConfigFeature from "./features/logic-config-feature";


/**
Expand All @@ -56,6 +52,11 @@ appContainer.bind<DIDGatewayOutputPort>(GATEWAYS.DID).to(RucioDIDGateway);
appContainer.bind<EnvConfigGatewayOutputPort>(GATEWAYS.ENV_CONFIG).to(EnvConfigGateway);
appContainer.bind<StreamGatewayOutputPort>(GATEWAYS.STREAM).to(StreamingGateway).inRequestScope();

loadFeaturesSync(appContainer, [
new ListDidsFeature(appContainer),
new LoginConfigFeature(appContainer)
])

appContainer.bind<UserPassLoginInputPort>(INPUT_PORT.USERPASS_LOGIN).to(UserPassLoginUseCase).inRequestScope();
appContainer.bind<IUserPassLoginController>(CONTROLLERS.USERPASS_LOGIN).to(UserPassLoginController);
appContainer.bind<interfaces.Factory<UserPassLoginInputPort>>(USECASE_FACTORY.USERPASS_LOGIN).toFactory<UserPassLoginUseCase, [IronSession, NextApiResponse]>((context: interfaces.Context) =>
Expand All @@ -66,17 +67,6 @@ appContainer.bind<interfaces.Factory<UserPassLoginInputPort>>(USECASE_FACTORY.US
}
);

appContainer.bind<ListDIDsInputPort>(INPUT_PORT.LIST_DIDS).to(ListDIDsUseCase).inRequestScope();
appContainer.bind<BaseController<ListDIDsControllerParameters ,ListDIDsRequest>>(CONTROLLERS.LIST_DIDS).to(ListDIDsController);
appContainer.bind<interfaces.Factory<ListDIDsInputPort>>(USECASE_FACTORY.LIST_DIDS).toFactory<ListDIDsUseCase, [NextApiResponse]>((context: interfaces.Context) =>
(response: NextApiResponse) => {
const rucioDIDGateway: DIDGatewayOutputPort = appContainer.get(GATEWAYS.DID)
return new ListDIDsUseCase(new ListDIDsPresenter(response), rucioDIDGateway);
}
);

loadFeatures(appContainer)

appContainer.bind<SetX509LoginSessionInputPort>(INPUT_PORT.SET_X509_LOGIN_SESSION).to(SetX509LoginSessionUseCase).inRequestScope();
appContainer.bind<ISetX509LoginSessionController>(CONTROLLERS.SET_X509_LOGIN_SESSION).to(SetX509LoginSessionController);
appContainer.bind<interfaces.Factory<SetX509LoginSessionInputPort>>(USECASE_FACTORY.SET_X509_LOGIN_SESSION).toFactory<SetX509LoginSessionUseCase, [IronSession, NextApiResponse]>((context: interfaces.Context) =>
Expand Down
47 changes: 47 additions & 0 deletions src/lib/infrastructure/ioc/features/list-dids-feature.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import DIDGatewayOutputPort from '@/lib/core/port/secondary/did-gateway-output-port'
import {
ListDIDsError,
ListDIDsRequest,
ListDIDsResponse,
} from '@/lib/core/usecase-models/list-dids-usecase-models'
import { ListDIDsControllerParameters } from '@/lib/infrastructure/controller/list-dids-controller'
import { ListDIDsViewModel } from '@/lib/infrastructure/data/view-model/list-did'
import {
BaseStreamableFeature,
IFeature,
IOCSymbols,
} from '@/lib/sdk/ioc-helpers'
import GATEWAYS from '@/lib/infrastructure/ioc/ioc-symbols-gateway'
import CONTROLLERS from '@/lib/infrastructure/ioc/ioc-symbols-controllers'
import INPUT_PORT from '@/lib/infrastructure/ioc/ioc-symbols-input-port'
import USECASE_FACTORY from '@/lib/infrastructure/ioc/ioc-symbols-usecase-factory'
import { Container } from 'inversify'
import ListDIDsController from '@/lib/infrastructure/controller/list-dids-controller'
import ListDIDsUseCase from '@/lib/core/use-case/list-dids-usecase'
import ListDIDsPresenter from '../../presenter/list-dids-presenter'

export default class ListDidsFeature extends BaseStreamableFeature<
ListDIDsControllerParameters,
ListDIDsRequest,
ListDIDsResponse,
ListDIDsError,
ListDIDsViewModel
> {
constructor(appContainer: Container) {
const didGateway = appContainer.get<DIDGatewayOutputPort>(GATEWAYS.DID)
const symbols: IOCSymbols = {
CONTROLLER: CONTROLLERS.LIST_DIDS,
USECASE_FACTORY: USECASE_FACTORY.LIST_DIDS,
INPUT_PORT: INPUT_PORT.LIST_DIDS,
}
super(
'ListDIDs',
ListDIDsController,
ListDIDsUseCase,
[didGateway],
ListDIDsPresenter,
false,
symbols,
)
}
}
7 changes: 2 additions & 5 deletions src/lib/infrastructure/ioc/features/logic-config-feature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,15 @@ export default class LoginConfigFeature extends BaseFeature<
LoginConfigError,
LoginViewModel
> {
constructor(
appContainer: Container,

) {
constructor(appContainer: Container) {
const envGateway: EnvConfigGateway = appContainer.get<EnvConfigGatewayOutputPort>(GATEWAYS.ENV_CONFIG)
const symbols: IOCSymbols = {
CONTROLLER: CONTROLLERS.LOGIN_CONFIG,
USECASE_FACTORY: USECASE_FACTORY.LOGIN_CONFIG,
INPUT_PORT: INPUT_PORT.LOGIN_CONFIG,
}
super(
appContainer,
"LoginConfig",
LoginConfigController,
LoginConfigUseCase,
[
Expand Down
181 changes: 160 additions & 21 deletions src/lib/sdk/ioc-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import { IronSession } from 'iron-session';
import { NextApiResponse } from 'next';
import path from 'path';
import { BaseController, TParameters } from './controller';
import { BasePresenter } from './presenter';
import type { BaseInputPort, BaseOutputPort } from './primary-ports';
import { BasePresenter, BaseStreamingPresenter } from './presenter';
import type { BaseInputPort, BaseOutputPort, BaseStreamingOutputPort } from './primary-ports';
import { TUseCase } from './usecase';
import TUseCaseFactory from './usecase-factory';
import { BaseResponseModel } from './usecase-models';
import { BaseViewModel } from './view-models';

/**
* An object that contains symbols for the different types of dependencies in an IoC container.
Expand All @@ -18,6 +20,20 @@ export type IOCSymbols = {
INPUT_PORT: symbol, // A symbol for the input port dependency.
}


/**
* A base interface for loadable features in the web application.
*/
export interface IFeature{
name: string
/**
* Load the feature into the IoC container.
* @param appContainer The IoC container for the application.
*/
load(appContainer: Container): void;
}


/**
* A base class for features in a web application. The IOC bindings for the clean architecture components
* of the feature are generated automatically.
Expand All @@ -34,7 +50,7 @@ TControllerParams extends TParameters,
TErrorModel,
TViewModel,
>
{
implements IFeature {
/**
* Creates a new instance of the `BaseFeature` class.
* @template TControllerParams The type of the parameters for the controller.
Expand All @@ -51,22 +67,28 @@ TControllerParams extends TParameters,
* @param symbols An object that contains symbols for the different types of dependencies in the IoC container.
*/
public constructor(
appContainer: Container,
Controller: new (useCaseFactory: TUseCaseFactory<TRequestModel>) => BaseController<TControllerParams, TRequestModel>,
UseCase: new (presenter: BaseOutputPort<TResponseModel, TErrorModel>, ...args: any[]) => TUseCase<TRequestModel>,
useCaseContructorArgs: any[] = [],
Presenter: new (response: NextApiResponse, session?: IronSession) => BasePresenter<TResponseModel, TErrorModel, TViewModel>,
passSessionToPresenter: boolean = false,
symbols: IOCSymbols,
) {
public name: string,
private Controller: new (useCaseFactory: TUseCaseFactory<TRequestModel>) => BaseController<TControllerParams, TRequestModel>,
private UseCase: new (presenter: BaseOutputPort<TResponseModel, TErrorModel>, ...args: any[]) => TUseCase<TRequestModel>,
private useCaseContructorArgs: any[] = [],
private Presenter: new (response: NextApiResponse, session?: IronSession) => BasePresenter<TResponseModel, TErrorModel, TViewModel>,
private passSessionToPresenter: boolean = false,
private symbols: IOCSymbols,
) {}

/**
* Load this feature into the IoC container.
* @param appContainer The IoC container for the application.
*/
load(appContainer: Container): void {
this.createIOCBindings<TControllerParams, TRequestModel, TResponseModel, TErrorModel, TViewModel>(
appContainer,
Controller,
UseCase,
useCaseContructorArgs,
Presenter,
passSessionToPresenter,
symbols,
this.Controller,
this.UseCase,
this.useCaseContructorArgs,
this.Presenter,
this.passSessionToPresenter,
this.symbols,
)
}

Expand Down Expand Up @@ -125,10 +147,113 @@ TControllerParams extends TParameters,
}
}

/**
* A base class for streamable features in a web application.
* @template TControllerParams The type of the parameters for the controller.
* @template TRequestModel The type of the request model for the use case.
* @template TResponseModel The type of the response model for the use case.
* @template TErrorModel The type of the error model for the use case.
* @template TViewModel The type of the view model for the presenter.
*/
export class BaseStreamableFeature<
TControllerParams extends TParameters,
TRequestModel,
TResponseModel extends BaseResponseModel,
TErrorModel,
TViewModel extends BaseViewModel,
> implements IFeature{
/**
* Creates a new instance of the `BaseStreamableFeature` class.
* @param appContainer The IoC container for the application.
* @param Controller The controller class for the feature.
* @param UseCase The use case class for the feature.
* @param useCaseContructorArgs The arguments to pass to the use case constructor.
* @param Presenter The presenter class for the feature.
* @param passSessionToPresenter Whether to pass the session to the presenter.
* @param symbols An object that contains symbols for the different types of dependencies in the IoC container.
*/
constructor(
public name: string,
private Controller: new (useCaseFactory: TUseCaseFactory<TRequestModel>) => BaseController<TControllerParams, TRequestModel>,
private UseCase: new (presenter: BaseStreamingOutputPort<TResponseModel, TErrorModel>, ...args: any[]) => TUseCase<TRequestModel>,
private useCaseContructorArgs: any[] = [],
private Presenter: new (response: NextApiResponse, session?: IronSession) => BaseStreamingPresenter<TResponseModel, TViewModel, TErrorModel>,
private passSessionToPresenter: boolean = false,
private symbols: IOCSymbols,
) {}

/**
* Load this feature into the IoC container.
* @param appContainer The IoC container for the application.
*/
public load(appContainer: Container): void {
this.createIOCBindings<TControllerParams, TRequestModel, TResponseModel, TErrorModel, TViewModel>(
appContainer,
this.Controller,
this.UseCase,
this.useCaseContructorArgs,
this.Presenter,
this.passSessionToPresenter,
this.symbols,
)
}

/**
* Creates the IoC bindings for the streamable feature.
* @param appContainer The IoC container for the application.
* @param Controller The controller class for the feature.
* @param UseCase The use case class for the feature.
* @param useCaseContructorArgs The arguments to pass to the use case constructor.
* @param Presenter The presenter class for the feature.
* @param passSessionToPresenter Whether to pass the session to the presenter.
* @param symbols An object that contains symbols for the different types of dependencies in the IoC container.
*/
createIOCBindings<
TControllerParams extends TParameters,
TRequestModel,
TResponseModel extends BaseResponseModel,
TErrorModel,
TViewModel extends BaseViewModel,
>(
appContainer: Container,
Controller: new (useCaseFactory: TUseCaseFactory<TRequestModel>) => BaseController<TControllerParams, TRequestModel>,
UseCase: new (presenter: BaseStreamingOutputPort<TResponseModel, TErrorModel>, ...args: any[]) => TUseCase<TRequestModel>,
useCaseContructorArgs: any[] = [],
Presenter: new (response: NextApiResponse, session?: IronSession) => BaseStreamingPresenter<TResponseModel, TViewModel, TErrorModel>,
passSessionToPresenter: boolean = false,
symbols: IOCSymbols,
) {
const symbolInputPort = symbols.INPUT_PORT
const symbolController = symbols.CONTROLLER
const symbolUseCaseFactory = symbols.USECASE_FACTORY

appContainer.bind<BaseInputPort<TRequestModel>>(symbolInputPort).to(UseCase).inRequestScope();
appContainer.bind<BaseController<TControllerParams, TRequestModel>>(symbolController).to(Controller).inRequestScope();

if(passSessionToPresenter){
appContainer.bind<interfaces.Factory<TUseCase<TRequestModel>>>(symbolUseCaseFactory).toFactory<TUseCase<TRequestModel>, [response: NextApiResponse, session: IronSession]>((context: interfaces.Context) =>
(response: NextApiResponse, session: IronSession) => {
const presenter = new Presenter(response, session);
return new UseCase(presenter, ...useCaseContructorArgs);
}
);
} else {
appContainer.bind<interfaces.Factory<TUseCase<TRequestModel>>>(symbolUseCaseFactory).toFactory<TUseCase<TRequestModel>, [response: NextApiResponse]>((context: interfaces.Context) =>
(response: NextApiResponse) => {
const presenter = new Presenter(response);
return new UseCase(presenter, ...useCaseContructorArgs);
});
}
}
}

/**
* Loads features from the features directory into the IoC Container.
* @param appContainer The IoC container for the application.
* @param featuresDir The directory to load features from. Defaults to `src/lib/infrastructure/ioc/features`.
* @deprecated NextJS Compiler does not support server side dynamic imports.
* The modules cannot be found at runtime as .next directory contains its own dynnamic file structure.
* Use loadFeaturesSync instead.
*/
export async function loadFeatures(appContainer: Container, featuresDir?: string) {
const FEATURES_PATH = featuresDir || path.join(process.cwd(), 'src', 'lib', 'infrastructure', 'ioc', 'features');
Expand All @@ -144,9 +269,10 @@ export async function loadFeatures(appContainer: Container, featuresDir?: string
if (!featureClass) {
throw new Error(`Feature ${featureName} has no default export`)
}
// if default export is not a subclass of BaseFeature, throw error
if (!(featureClass.prototype instanceof BaseFeature)) {
throw new Error(`Feature ${featureName} is not a subclass of BaseFeature`)
// if default export is not a subclass of BaseFeature or BaseStreambleFeature, throw error
if (!(featureClass.prototype instanceof BaseFeature) &&
!(featureClass.prototype instanceof BaseStreamableFeature)) {
throw new Error(`Feature ${featureName} is not a subclass of BaseFeature or BaseStreamableFeature`)
}
// if constructor signature of default export is not new (appContainer: Container) => BaseFeature, throw error
if (featureClass.length !== 1) {
Expand All @@ -160,4 +286,17 @@ export async function loadFeatures(appContainer: Container, featuresDir?: string
throw error;
}
}
}
}

export function loadFeaturesSync(appContainer: Container, features: IFeature[]) {
console.log(`Loading ${features.length} features`)
for ( const feature of features ) {
try {
console.log(`Loading feature ${feature.name}`)
feature.load(appContainer);
} catch (error) {
console.error(`Error loading feature ${feature.name}: ${error}`)
throw error;
}
}
}
34 changes: 34 additions & 0 deletions src/pages/api/dids.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import appContainer from "@/lib/infrastructure/ioc/container-config";
import CONTROLLERS from "@/lib/infrastructure/ioc/ioc-symbols-controllers";
import { BaseController } from "@/lib/sdk/controller";
import { NextApiRequest, NextApiResponse } from "next";
import { ListDIDsControllerParameters } from "@/lib/infrastructure/controller/list-dids-controller";
import { withAuthenticatedSessionRoute } from "@/lib/infrastructure/auth/session-utils";

async function listDIDs(req: NextApiRequest, res: NextApiResponse, rucioAuthToken: string) {
if(req.method === 'GET') {
res.status(405).json({ error: 'Method Not Allowed' })
return
}
const { query, type } = req.body
if(!query) {
res.status(400).json({ error: 'Missing query parameter' })
return
}
if(!type) {
res.status(400).json({ error: 'Missing type parameter' })
return
}

const controller = appContainer.get<BaseController<ListDIDsControllerParameters, void>>(CONTROLLERS.LIST_DIDS)
const controllerParameters: ListDIDsControllerParameters = {
response: res,
query: query,
type: type,
rucioAuthToken: rucioAuthToken
}

await controller.execute(controllerParameters)
}

export default withAuthenticatedSessionRoute(listDIDs)