Create scoped module with providers for module feature - javascript

When I use the import PolicyModule.forFeature more than one time, the next import of the PolicyModule overrides gates in PolicyStorage.
When I try to use PolicyProvider in CandidateModule's CandidateEducationService by calling PolicyProvider
await this.policy.denyAccessUnlessGranted('canDelete', education);
I get the exception Gate by entity 'CandidateEducationEntity' not found.
I output PolicyStorage in CandidateEducationService and got array gates with JobPolicy
PolicyStorage {
gates:
[ { policy: [Function: JobPolicy], entity: [Function: JobEntity] } ]
}
But I was expecting
PolicyStorage {
gates:
[ { policy: [Function: CandidateEducationPolicy], entity: [Function: CandidateEducationEntity] } ]
}
I created a dynamic module PolicyModule
#Module({})
export class PolicyModule {
public static forFeature(gates: PolicyGate[]): DynamicModule {
const providers: Provider[] = [
...gates.map(gate => gate.policy),
{
provide: PolicyStorage,
useValue: new PolicyStorage(gates),
},
PolicyProvider,
];
return {
module: PolicyModule,
imports: [
CommonModule,
],
providers,
exports: providers,
};
}
}
PolicyStorage
#Injectable()
export class PolicyStorage {
constructor(private gates: PolicyGate[]) {
console.log(this.gates);
}
public find(name: string): PolicyGate | null {
return this.gates.find(policy => policy.entity.name === name);
}
}
PolicyProvider
#Injectable()
export class PolicyProvider<E, P> {
constructor(
private readonly moduleRef: ModuleRef,
private readonly gateStorage: PolicyStorage,
private readonly appContext: AppContextService,
) {
}
public async denyAccessUnlessGranted(methodNames: MethodKeys<P>, entity: E, customData?: any) {
if (await this.denies(methodNames, entity, customData)) {
throw new ForbiddenException();
}
}
public async allowAccessIfGranted(methodNames: MethodKeys<P>, entity: E, customData?: any) {
const allowed = await this.allows(methodNames, entity, customData);
if (!allowed) {
throw new ForbiddenException();
}
}
private async allows(methodNames: MethodKeys<P>, entity: E, customData?: any): Promise<boolean> {
const results = await this.getPolicyResults(methodNames, entity, customData);
return results.every(res => res === true);
}
private async denies(methodNames: MethodKeys<P>, entity: E, customData?: any): Promise<boolean> {
const results = await this.getPolicyResults(methodNames, entity, customData);
return results.every(res => res === false);
}
private async getPolicyResults(methodNames: MethodKeys<P>, entity: E, customData?: any): Promise<boolean[]> {
const methodNamesArray = Array.isArray(methodNames) ? methodNames : [methodNames];
const gate = this.findByClassName(entity.constructor.name);
const user = this.appContext.get('user');
const policy = await this.moduleRef.get<P>(gate.policy, {strict: false});
const results = [];
for (const methodName of methodNamesArray) {
results.push(!!await policy[methodName as string](entity, user, customData));
}
return results;
}
private findByClassName(name: string) {
const gate = this.gateStorage.find(name);
if (!gate) {
throw new RuntimeException(`Gate by entity '${name}' not found`);
}
return gate;
}
}
Using module in other module. Example:
JobsModule
#Module({
imports: [
TypeOrmModule.forFeature(
[
JobEntity,
],
),
PolicyModule.forFeature([
{
policy: JobPolicy,
entity: JobEntity,
},
]),
],
controllers: [
ManagerJobsController,
],
providers: [
ManagerJobsService,
],
})
export class JobsModule {
}
CandidateModule
#Module({
imports: [
TypeOrmModule.forFeature(
[
CandidateEducationEntity,
],
),
PolicyModule.forFeature([
{
policy: CandidateEducationPolicy,
entity: CandidateEducationEntity,
},
]),
],
controllers: [
CandidateEducationController,
],
providers: [
CandidateEducationService,
],
})
export class CandidateModule {
}

Update:
Nest v6 introduced request-scoped providers, see this answer.
All modules and its providers are singletons. If you register a provider under the same token twice within the same module, it will be overridden.
If you have a look at the TypeOrmModule you can see it registers its repository providers under a unique custom token for each entity:
export function getRepositoryToken(entity: Function) {
if (
entity.prototype instanceof Repository ||
entity.prototype instanceof AbstractRepository
) {
return getCustomRepositoryToken(entity);
}
return `${entity.name}Repository`;
}
So in your case, you could have the functions getPolicyProviderToken and getPolicyStorageToken and both register and inject your providers under these tokens that are unique for each importing module.

Related

Typing is not checked for classes returned by factory function in typescript

I've created a module factory function with returns a class annotated by #Module({}). My problem is, the class depends on function arguments providerToken and strategy so I cannot move it outside the function. When I run the code, it works perfectly fine but the value for forRoot and forRootAsync is not properly checked for types. Infact typescript doesn't throw any error about that. Also what should be my return value of the function. I've put it any to avoid errors for now.
This is how I'm using the function to create a module
const TwitterAuthModule =
createHybridAuthModule<TwitterAuthModuleOptions>(
TWITTER_HYBRID_AUTH_OPTIONS,
TwitterAuthStrategy
);
Module creator factory
export function createHybridAuthModule<T>(
providerToken: string,
strategy: any
): any {
#Module({})
class NestHybridAuthModule {
static forRoot(options: T): DynamicModule {
return {
module: NestHybridAuthModule,
providers: [
{
provide: providerToken,
useValue: options,
},
strategy,
],
};
}
static forRootAsync(
options: ModuleAsyncOptions<ModuleOptionsFactory<T>, T>
): DynamicModule {
return {
module: NestHybridAuthModule,
providers: [...this.createAsyncProviders(options), strategy],
};
}
private static createAsyncProviders(
options: ModuleAsyncOptions<ModuleOptionsFactory<T>, T>
): Provider[] {
if (options.useExisting || options.useFactory) {
return [this.createAsyncOptionsProvider(options)];
}
const useClass = options.useClass as Type<ModuleOptionsFactory<T>>;
return [
this.createAsyncOptionsProvider(options),
{
provide: useClass,
useClass,
},
];
}
private static createAsyncOptionsProvider(
options: ModuleAsyncOptions<ModuleOptionsFactory<T>, T>
): Provider {
if (options.useFactory) {
return {
provide: providerToken,
useFactory: options.useFactory,
inject: options.inject || [],
};
}
const inject = [
(options.useClass || options.useExisting) as Type<
ModuleOptionsFactory<T>
>,
];
return {
provide: providerToken,
useFactory: async (optionsFactory: ModuleOptionsFactory<T>) =>
await optionsFactory.createModuleOptions(),
inject,
};
}
}
return NestHybridAuthModule;
}
Usage of the created module. The value of forRoot is never checked for types
#Module({
imports: [
TwitterAuthModule.forRoot({
consumerKey: '********',
consumerSecret: '******',
callbackURL: '*******',
}),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
I finally found my answer at https://stackoverflow.com/a/43674389/1029506 and implemented it using a customer decorator:
export interface INestHybridAuthModule<T> {
forRoot(options: T): DynamicModule;
}
function staticImplements<T>() {
return <U extends T>(constructor: U) => {
constructor;
};
}
function createHybridAuthModule<T>(
providerToken: string,
strategy: any
): INestHybridAuthModule<T> {
#Module({})
#staticImplements<INestHybridAuthModule<T>>()
class NestHybridAuthModule {
static forRoot(options: T): DynamicModule {
// code here
}
static forRootAsync(
options: ModuleAsyncOptions<ModuleOptionsFactory<T>, T>
): DynamicModule {
// Code here
}
private static createAsyncProviders(
options: ModuleAsyncOptions<ModuleOptionsFactory<T>, T>
): Provider[] {
// Code here
}
private static createAsyncOptionsProvider(
options: ModuleAsyncOptions<ModuleOptionsFactory<T>, T>
): Provider {
// Code here
}
}
return NestHybridAuthModule;
}
And Finally
const TwitterAuthModule: INestHybridAuthModule<TwitterAuthModuleOptions> = createHybridAuthModule<
TwitterAuthModuleOptions
>(TWITTER_HYBRID_AUTH_OPTIONS, TwitterAuthStrategy);
Reason why typescript not check your types is that it treat NestHybridAuthModule as any because createHybridAuthModule function return type is any. Remove it and allow typescript infer return type.
In case there is --noImplicitReturns used you can use generic interface
Edit:
Thanks for pointing it.
corrected example:
interface INestHybridAuthModule<T> {
new(...args: any[]): any,
forRoot(options: T): DynamicModule
}
export function createHybridAuthModule<T>(
providerToken: string,
strategy: any
): INestHybridAuthModule<T> {
class NestHybridAuthModule {
static forRoot(options: T): DynamicModule {
// logic here
}
}
return NestHybridAuthModule
}
original:
simplified example:
interface INestHybridAuthModule<T> {
forRoot(options: T): DynamicModule
}
export function createHybridAuthModule<T>(
providerToken: string,
strategy: any
): new (...args: any[]) => INestHybridAuthModule<T> {
class NestHybridAuthModule {
forRoot(options: T): DynamicModule {
// logic here
}
}
return NestHybridAuthModule
}

NestJs - mongoose - Dynamic collection naming

I'd like to use dynamic collection names based on current year.
For example: From 'products' to 'products2020'.
Using NESTJS, I have to import "module.forFeature" with an specifyc collection name.
import { Module } from '#nestjs/common'
import { MongooseModule } from '#nestjs/mongoose'
#Module({
imports: [
MongooseModule.forFeature([
{
name: 'Products',
schema: ProductsSchema
}
])
],
controllers: [ProductsController],
providers: [ProductsService]
})
And the same happens with injection at service:
import { Injectable } from '#nestjs/common'
import { InjectModel } from '#nestjs/mongoose'
import { Model } from 'mongoose'
#Injectable()
export class ProductsService {
constructor(
#InjectModel('Products')
private readonly productsModel: Model<Products>
) {}
}
And finally, here's my schema:
import { Schema } from 'mongoose'
export const ProductsSchema = new Schema(
{
_id: { Type: String, required: true },
code: String
},
{
collection: 'Products'
}
)
Is there some way to achieve dynamic naming?
Thanks a lot !
I stumble into a similar issue, and the way I resolved was using the MongooseModule.forFeatureAsync method. The model and schema declaration are the same as in the nestjs docs.
#Module({
imports: [
MongooseModule.forFeatureAsync([
{
name: UsersModel.name,
imports: [EnvironmentModule],
inject: [EnvironmentService],
useFactory: (envService: EnvironmentService) => {
const env = envService.getEnv();
const schema = UsersSchema.set(
'collection',
`${env.countryCode}-users`,
);
return schema;
},
},
]),
...
],
providers: []
...
I've looking for a solution to this kind of problem but i've hit a wall and there was no clear way to do it.
Below (minimal) code instantiate Services each bound to a specific model depending on a country parameter. i.e ServiceX bound to Model of Database X, ServiceY bound to the same Model in Database Y
But here is what i managed to do. You can absolutely do a work around to fit your needs
First comes the model/interface. Commonly used between different services
export interface User extends Document {
readonly username: string;
readonly password: string;
}
export const UserSchema = new mongoose.Schema(
{
_id: mongoose.ObjectId,
username: String,
password: String
},
{ collection: 'accounts', autoCreate: true }
);
Service definition is indeed the same for every model in different database/collection
#Injectable()
export class XUserService implements OnModuleInit{
constructor(
private userModel: Model<User>,
) {
}
////////////////////////////////////////////////////////////////////////////
async onModuleInit(): Promise<any> {
console.log(`inside service dbname=: ${this.userModel.db.name} > ${this.userModel.collection.collectionName}` );
// await new this.userModel({_id: mongoose.Types.ObjectId(), username: 'test', password: 'test', flag: this.c}).save()
}
async insert(){
console.log(`inside service dbname=: ${this.userModel.db.name} > ${this.userModel.collection.collectionName}` );
await new this.userModel({
_id: mongoose.Types.ObjectId(),
username: this.userModel.db.name,
password: '0000'
}).save();
}
async findOne(): Promise<User>{
console.log(`inside service in : ${this.userModel.db.name} > ${this.userModel.collection.collectionName}` );
return this.userModel.findOne()
}
}
For Module, i made a DynamicModule
Import DBConnections
Create a Model for each need, ( for my case, one model in each Database )
Create and bind each Model to a Service, so the instantiation of the service will be correct
#Module({
})
export class XUserModule{
static register( /*use can pass parameter here*/): DynamicModule{
return{
module: XUserModule,
imports: [
DatabaseModule
],
controllers: [
XUserController
],
providers: [
// Create Models here, #1 and #2 in two different database
{
provide: 'dz-'+'UserModel',
useFactory: (connection: Connection)=> {
return connection.model('User', UserSchema )
},
inject: [ dbname.shooffood('dz')+'Connection' ]
},{
provide: 'ca-'+'UserModel',
useFactory: (connection: Connection)=> {
return connection.model('User', UserSchema )
},
inject: [ dbname.shooffood('ca')+'Connection' ]
},
// Create Providers/Services for each Model and Inject the Model to the Service by `TokenString`
{
provide: 'dz' + XUserService.name,
useFactory: (m: any)=> {
console.log(m);
return new XUserService(m);
},
inject: [ 'dz-'+'UserModel' ]
},{
provide: 'ca' + XUserService.name,
useFactory: (m: any)=> {
console.log(m);
return new XUserService(m);
},
inject: [ 'ca-'+'UserModel' ]
}
],
// Export your service with the same `provide` name for later usage.
exports: [
'dz' + XUserService.name,
'ca' + XUserService.name
]
}
}
}
Just FYI, database module looks like
Constants dbname are connection names and uri are the connection string.
const databaseProviders = [
{
provide: dbname.admin+'Connection',
useFactory: (): Promise<typeof mongoose> => mongoose.createConnection(uri.admin),
},{
provide: dbname.system+'Connection',
useFactory: (): Promise<typeof mongoose> => mongoose.createConnection(uri.system),
},{
provide: dbname.shooffood('dz')+'Connection',
useFactory: (): Promise<typeof mongoose> => mongoose.createConnection(uri.dzfood),
},{
provide: dbname.shooffood('ca')+'Connection',
useFactory: (): Promise<typeof mongoose> => mongoose.createConnection(uri.cafood),
}
];
#Module({
providers: [
...databaseProviders
],
exports: [
dbname.admin+'Connection',
dbname.system+'Connection',
dbname.shooffood('dz')+'Connection',
dbname.shooffood('ca')+'Connection'
]
})
export class DatabaseModule {}
As for Controller, there is only one that handle each service via request param :country. But first i had to list all possible Models and services to include in the Application.
#Controller(':country')
export class XUserController {
private byCountryServices = new Map();
constructor(
// Inject all required services by `tokenString`
#Inject('dz' + XUserService.name) private dzUserService: XUserService,
#Inject('ca' + XUserService.name) private caUserService: XUserService,
) {
// Add to `<key, value>` Map for easy by param access
this.byCountryServices.set('dz', this.dzUserService );
this.byCountryServices.set('ca', this.caUserService );
}
#Get('post')
async post(
#Param('country') c: string
): Promise<string>{
await this.byCountryServices.get(c).insert()
return 'inserted in ' + c;
}
#Get('get')
async get(
#Param('country') c: string
): Promise<string>{
console.log('param: ' + c)
return await this.byCountryServices.get(c).findOne()
}
}
Finally you import the module in AppModule with
XUserModule.register()

upgrading from angular 4 to 7 causing some problems

hey i have upgraded my project from angular 4 to angular 7 and some of the services, modules are deprecated.
this is my app.module.ts
#NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
BrowserAnimationsModule,
SharedModule,
HttpClientModule,
AppRoutingModule,
NgbModule.forRoot(),
StoreModule.forRoot({})
],
providers: [
SessionTimeoutService,
SpinnerService,
{
provide: HttpClient,
useFactory: httpFactory,
deps: [XHRBackend, RequestOptions, Store, SpinnerService]
},
UtilService,
{ provide: NgbDateParserFormatter, useClass: DateParserFormatter }
],
bootstrap: [AppComponent]
})
export class AppModule {
constructor(private injector: Injector) {
ServiceLocatorService.injector = this.injector;
}
}
now XHRBackend, RequestOptions are now deprecated and giving me error
can someone tell me how to resolve it?
and this is my Http interceptor file
#Injectable()
export class InterceptedHttp extends HttpClient {
constructor(
backend: HttpBackend,
defaultOptions: RequestOptions,
private store: Store<any>,
private spinnerService: SpinnerService
) {
super(backend, defaultOptions);
}
request(
url: string | HttpRequest,
options?: RequestOptionsArgs
): Observable<HttpResponse> {
this.showLoader();
return this.tryCatch(super.request(url, options)).finally(() => {
this.hideLoader();
});
}
get(url: string, options?: RequestOptionsArgs): Observable<HttpResponse> {
url = this.updateUrl(url);
return this.tryCatch(super.get(url, this.getRequestOptionArgs(options)));
}
post(
url: string,
body: string,
options?: RequestOptionsArgs
): Observable<HttpResponse> {
url = this.updateUrl(url);
return this.tryCatch(
super.post(url, body, this.getRequestOptionArgs(options))
);
}
put(
url: string,
body: string,
options?: RequestOptionsArgs
): Observable<HttpResponse> {
url = this.updateUrl(url);
return this.tryCatch(
super.put(url, body, this.getRequestOptionArgs(options))
);
}
delete(url: string, options?: RequestOptionsArgs): Observable<HttpResponse> {
url = this.updateUrl(url);
return this.tryCatch(super.delete(url, this.getRequestOptionArgs(options)));
}
patch(
url: string,
body: any,
options?: RequestOptionsArgs
): Observable<HttpResponse> {
url = this.updateUrl(url);
return this.tryCatch(
super.patch(url, body, this.getRequestOptionArgs(options))
);
}
private updateUrl(req: string) {
return environment.origin + req;
}
private getRequestOptionArgs(
options?: RequestOptionsArgs
): RequestOptionsArgs {
if (options == null) {
options = new RequestOptions();
}
if (options.headers == null) {
options.headers = new HttpHeaders();
}
options.headers.append("Content-Type", "application/json");
options.headers.append(
"Authorization",
` Bearer ${sessionStorage.AccessToken}`
);
return options;
}
}
i am getting errors
RequestOptions,
RequestOptionsArgs,
these are deprecated now i am getting errors how to resolve it?
You have to use new packge from #angular/common/http
like
import { HttpClientModule } from "#angular/common/http";
import { HttpClient } from "#angular/common/http";
So mostly you will use HttpHeaders to construct your ajax header like params formdata etc...
Headers -> HttpHeaders
Response -> HttpResponse
RequestOptions, RequestOptionsArgs are remove and you have to use HttpParams
Please read new change log here

`ng test` shows Error: Can't resolve all parameters for BackendService

Below error shown when I ran ng test command.
Here is my service spec,
describe('BackendService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
{
provide: Http, useFactory: (backend, options) => {
return new Http(backend, options);
},
deps: [MockBackend, BaseRequestOptions]
},
MockBackend,
BaseRequestOptions,
BackendService
]
});
});
it('should ...', inject([BackendService, MockBackend], (service: BackendService) => {
expect(service).toBeTruthy();
})
);
});
BackendService.ts looks like,
export class BackendService {
private baseUrl: string = 'https://foo-backend.appspot.com/_ah/api/default/v1';
constructor(private http: Http, baseName: string) {
this.baseUrl = this.baseUrl + baseName;
}
.....
}
It seems like extra parameter inside the BackendService class's constructor causes this problem..
How do you expect Angular to know what baseName is supposed to be? All constructor parameters need to be obtained from the Injector. And if there is no corresponding token for the parameter, then it can't be looked up.
You can add a token by doing
// somewhere in some file
import { OpaqueToken } from '#angular/core';
export const BASE_NAME_TOKEN = new OpaqueToken("app.base_name");
// in test class
import { BASE_NAME_TOKEN } from 'where-ever'
TestBed.configureTestingModule({
providers: [
BackendService,
{ provide: BASE_NAME_TOKEN, useValue: 'whatever-the-base-is' }
]
});
// in service constructor
import { Inject } from '#angular/core'
import { BASE_NAME_TOKEN } from 'where-ever'
constructor(http: Http, #Inject(BASE_NAME_TOKEN) baseName: string) {}
See Also:
Dependency Injection Tokens
What is the difference between #Inject vs constructor injection as normal parameter in Angular 2?

What is the correct way to convert an Angular 1 provider to Angular 2

There are lot of documentation and examples on how to convert Angular 1 services and factories to Angular2 but I couldnt find anything on how to convert a ng1 provider to something equivalent in ng2.
Example provider
function AlertService () {
this.toast = false;
this.$get = getService;
this.showAsToast = function(isToast) {
this.toast = isToast;
};
getService.$inject = ['$timeout', '$sce'];
function getService ($timeout, $sce) {
var toast = this.toast,
alertId = 0, // unique id for each alert. Starts from 0.
alerts = []
return {
factory: factory,
add: addAlert
};
function factory(alertOptions) {
var alert = {
type: alertOptions.type,
msg: $sce.trustAsHtml(alertOptions.msg),
id: alertOptions.alertId,
toast: alertOptions.toast
};
alerts.push(alert);
return alert;
}
function addAlert(alertOptions) {
alertOptions.alertId = alertId++;
var alert = this.factory(alertOptions);
return alert;
}
}
}
angular
.module('angularApp', [])
.provider('AlertService', AlertService);
What would be the correct equivalent for this in Angular 2?
Ok so finally we figured it out thanks to https://github.com/jhipster/generator-jhipster/issues/3664#issuecomment-251902173
Here is the Service in NG2
import {Injectable, Sanitizer, SecurityContext} from '#angular/core';
#Injectable()
export class AlertService {
private alertId: number;
private alerts: any[];
constructor(private sanitizer: Sanitizer, private toast: boolean) {
this.alertId = 0; // unique id for each alert. Starts from 0.
this.alerts = [];
}
factory(alertOptions): any {
var alert = {
type: alertOptions.type,
msg: this.sanitizer.sanitize(SecurityContext.HTML, alertOptions.msg),
id: alertOptions.alertId,
toast: alertOptions.toast
};
this.alerts.push(alert);
return alert;
}
addAlert(alertOptions, extAlerts): any {
alertOptions.alertId = this.alertId++;
var alert = this.factory(alertOptions);
return alert;
}
isToast(): boolean {
return this.toast;
}
}
and here is the provider for the service
import { Sanitizer } from '#angular/core';
import { AlertService } from './alert.service';
export function alertServiceProvider(toast?: boolean) {
// set below to true to make alerts look like toast
let isToast = toast ? toast : false;
return {
provide: AlertService,
useFactory: (sanitizer: Sanitizer) => new AlertService(sanitizer, isToast),
deps: [Sanitizer]
}
}
Now you need to call the alertServiceProvider method in the provider declaration of your module.
#NgModule({
imports: [
...
],
declarations: [
...
],
providers: [
...
alertServiceProvider()
],
exports: [
...
]
})
export class SharedCommonModule {}
The code is part of the JHipster project and you can browse actual templates here

Categories

Resources