NestJs - mongoose - Dynamic collection naming - javascript

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()

Related

Create scoped module with providers for module feature

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.

How do I write simple test to check the behaviour of MatDialog in Angular 2?

I have the following component:
#Component({
selector: 'my-form',
templateUrl: './my-form.component.html',
})
export class MyFormComponent implements OnInit {
#Input('company') company: CompanyInfo;
private config: ConfigInterface | null;
constructor(private companyService: CompanyService, private something: Something, private dialog: MatDialog) {
}
ngOnInit() {
....
}
theMtehodWhichIWantToTest(company: string) {
if (someTest.indexOf(domain)) {
this.dialog.open(MyAllertComponent, {
data: {
title: 'blah',
},
});
}
}
}
My poor and failing unit test:
describe( 'MyFormComp', () => {
it('should open MatDialog if email is from popular domain', () => {
const dialog = {} as MatDialog;
const comp = new MyComponent({} as CompanyService, {} as Something, dialog);
comp.getCompanys(company);
expect(dialog.open(AlertComponent));
});
})
The error message: TypeError: dialog.open is not a function
Yes, I know why I have this error message - I am not mocking the dialog correctly and the functon open is not available. Can someone tell me how to make open available using jasmine (I.e. how to properly mock MatDialog?)
I am quite new to js unit testing so, just telling me what to google will not be very helpful for me.
You can use the beforeEach method to create spy's for your component dependencies:
emailServiceSpy = jasmine.createSpyObj('EmailService', {});
somethingSpy = jasmine.createSpyObj('Something', {});
dialogSpy = jasmine.createSpyObj('MatDialog', ['open']);
Then provide these for your Testmoudle:
TestBed.configureTestingModule({
declarations: [YourComponent],
providers: [
{ provide: EmailService, useValue: emailServiceSpy },
{ provide: Something, useValue: somethingSpy },
{ provide: MatDialog, useValue: dialogSpy },
],
imports: [],
}).compileComponents();
And in your Test you can check if your spy-function get called:
expect(dialogSpy.open).toHaveBeenCalled();

Angular 2 how to pass config to provider from app.module.ts

I'm trying to make angular 2 service with config file passed through constructor so i can read the config after it initializes and start other part of the code. I'm trying to make config file in app.module.ts and trough provider use factory to pass it down to my service but i have no luck, I've been stuck searching for google answers but i can't find the right solution. My code is bellow, i created config
app.module.ts
const config = new AuthServiceConfig([
{
id: FacebookLoginProvider.PROVIDER_ID,
provider: new FacebookLoginProvider(),
},
{
id: LinedinLoginProvider.PROVIDER_ID,
provider: new LinedinLoginProvider(),
},
]);
export function provideConfig() {
return config;
}
export function configFactory(config: AuthServiceConfig) {
return config;
}
providers: [
...
{ provide: AuthServiceConfig, useFactory: provideConfig },
{ provide: AuthProvider, useFactory: config },
],
auth.ts
export interface AuthServiceConfigItem {
id: string;
provider: Provider;
}
export class AuthServiceConfig {
providers: Map<string, Provider> = new Map<string, Provider>();
constructor(providers: AuthServiceConfigItem[]) {
for (let i = 0; i < providers.length; i++) {
let element = providers[i];
this.providers.set(element.id, element.provider);
}
}
}
#Injectable()
export class AuthProvider {
private static readonly ERR_LOGIN_PROVIDER_NOT_FOUND =
'Login provider not found';
providers: Map<string, Provider> = new Map<string, Provider>();
constructor(config) {
this.providers = config.providers;
console.log('Ovo su providersi u konstruktoru', this.providers);
}
I haven't tried dynamically change providers, but I used the Angular CLI environment files. Based on some flag from the environment file I would instantiate the proper service in the factory function.
Take a look at the app.module.ts and product.factory.ts here: https://github.com/Farata/angulartypescript/tree/master/code-samples/Angular6/chapter5/di-samples/src/app/factory

In Memory Web Api returns {data: Array[0]}

I'm trying to get some mocked results for my development environment. I've tried to incorporate angular-in-memory-web-api without much success. Here's my code:
app.module.ts:
#NgModule({
declarations: [
AppComponent,
],
imports: [
...
HttpModule,
...
InMemoryWebApiModule.forRoot(MockEventData, {
passThruUnknownUrl: true
})
],
providers: [
...
{
provide: Http,
useClass: ExtendedHttpService
},
...
{
provide: EventService,
useFactory: (http: Http, userService: UserService, newEventService: NewEventService, router: Router) => {
if (environment.production) {
return new EventService(http, userService, newEventService, router)
} else {
return new MockEventService(http, userService, newEventService, router)
}
},
deps: [Http, UserService, NewEventService, Router]
}
],
bootstrap: [AppComponent]
})
export class AppModule {
mock-event.service.ts:
#Injectable()
export class MockEventService {
private imageUploadBatch: Observable<Boolean>[];
private fakeResponse;
constructor(
private http: Http,
private userService: UserService,
private newEventService: NewEventService,
private router: Router,
) {
};
getEvents(excludedEvents: string[]): Observable<Event[]> {
return this.http
.post('api/events', excludedEvents)
.map((res: Response) => res.json())
.publishLast().refCount()
.catch((error: any) => Observable.throw(error.json().error || 'Show error.'));
}
}
mock-event-data.ts:
import { InMemoryDbService } from 'angular-in-memory-web-api';
export class MockEventData implements InMemoryDbService {
createDb() {
let events = [
{ id: 1, name: 'Windstorm' },
{ id: 2, name: 'Bombasto' },
{ id: 3, name: 'Magneta' },
{ id: 4, name: 'Tornado' }
];
return { events };
}
}
The code is quite simple. I made it following this guide: https://angular.io/docs/ts/latest/guide/server-communication.html. However, for whatever reason, the POST for /events always returns {data: Array[0]}.
Any help provided will be deeply appreciated.
Thanks!
post method will NOT just retrieve the data to angular-in-memory-web-api. Instead it will create the entity accordingly. It's essential and default behavior is to send data to the server same as put and delete. Of course there will be a response to the post request which is probably the data in angular-in-memory-web-api case but remember, this totally depends on the server to response. On the otherget should be used for the purpose of retrieving data from angular-in-memory-web-api as you want.
Well, as it turns out, POST seems to not return data in angular-in-memory-web-api. I succeeded in retrieving data by using a GET request. This isn't ideal, but it'll have to work for now.
If someone has a better answer, please provide it, as it's a bit iffy to commit a mock that doesn't use the original request types.
Thanks!

`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?

Categories

Resources