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

feat(core): add RouterModule (e.g., for versioning) #6035

Merged
merged 7 commits into from
Jan 27, 2021
Merged
Show file tree
Hide file tree
Changes from 5 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
77 changes: 77 additions & 0 deletions integration/hello-world/e2e/router-module-middleware.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import {
Controller,
Get,
INestApplication,
MiddlewareConsumer,
Module,
} from '@nestjs/common';
import { RouterModule } from '@nestjs/core';
import { Test } from '@nestjs/testing';
import * as request from 'supertest';
import { ApplicationModule } from '../src/app.module';

const RETURN_VALUE = 'test';
const SCOPED_VALUE = 'test_scoped';

@Controller()
class TestController {
@Get('test')
test() {
return RETURN_VALUE;
}

@Get('test2')
test2() {
return RETURN_VALUE;
}
}

@Module({
imports: [ApplicationModule],
controllers: [TestController],
})
class TestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply((req, res, next) => res.send(SCOPED_VALUE))
.forRoutes(TestController);
}
}

describe('RouterModule with Middleware functions', () => {
let app: INestApplication;

beforeEach(async () => {
app = (
await Test.createTestingModule({
imports: [
TestModule,
RouterModule.register([
{
path: '/module-path/',
module: TestModule,
},
]),
],
}).compile()
).createNestApplication();

await app.init();
});

it(`forRoutes(TestController) - /test`, () => {
return request(app.getHttpServer())
.get('/module-path/test')
.expect(200, SCOPED_VALUE);
});

it(`forRoutes(TestController) - /test2`, () => {
return request(app.getHttpServer())
.get('/module-path/test2')
.expect(200, SCOPED_VALUE);
});

afterEach(async () => {
await app.close();
});
});
99 changes: 99 additions & 0 deletions integration/hello-world/e2e/router-module.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { Controller, Get, INestApplication, Module } from '@nestjs/common';
import { RouterModule, Routes } from '@nestjs/core';
import { Test } from '@nestjs/testing';
import * as request from 'supertest';

describe('RouterModule', () => {
let app: INestApplication;

abstract class BaseController {
@Get()
getName() {
return this.constructor.name;
}
}

@Controller('/parent-controller')
class ParentController extends BaseController {}
@Controller('/child-controller')
class ChildController extends BaseController {}
@Controller('no-slash-controller')
class NoSlashController extends BaseController {}

class UnknownController {}
@Module({ controllers: [ParentController] })
class ParentModule {}

@Module({ controllers: [ChildController] })
class ChildModule {}

@Module({})
class AuthModule {}
@Module({})
class PaymentsModule {}

@Module({ controllers: [NoSlashController] })
class NoSlashModule {}

const routes1: Routes = [
{
path: 'parent',
module: ParentModule,
children: [
{
path: 'child',
module: ChildModule,
},
],
},
];
const routes2: Routes = [
{ path: 'v1', children: [AuthModule, PaymentsModule, NoSlashModule] },
];

@Module({
imports: [ParentModule, ChildModule, RouterModule.register(routes1)],
})
class MainModule {}

@Module({
imports: [
AuthModule,
PaymentsModule,
NoSlashModule,
RouterModule.register(routes2),
],
})
class AppModule {}

before(async () => {
const moduleRef = await Test.createTestingModule({
imports: [MainModule, AppModule],
}).compile();

app = moduleRef.createNestApplication();
await app.init();
});

it('should hit the "ParentController"', async () => {
return request(app.getHttpServer())
.get('/parent/parent-controller')
.expect(200, 'ParentController');
});

it('should hit the "ChildController"', async () => {
return request(app.getHttpServer())
.get('/parent/child/child-controller')
.expect(200, 'ChildController');
});

it('should hit the "NoSlashController"', async () => {
return request(app.getHttpServer())
.get('/v1/no-slash-controller')
.expect(200, 'NoSlashController');
});

afterEach(async () => {
await app.close();
});
});
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"format": "prettier \"**/*.ts\" --ignore-path ./.prettierignore --write && git status",
"postinstall": "opencollective",
"test": "nyc --require ts-node/register mocha packages/**/*.spec.ts --reporter spec --retries 3 --require 'node_modules/reflect-metadata/Reflect.js' --exit",
"test:integration": "mocha \"integration/*/{,!(node_modules)/**/}/*.spec.ts\" --reporter spec --require ts-node/register --require 'node_modules/reflect-metadata/Reflect.js' --exit",
"test:integration": "mocha \"integration/hello-world/{,!(node_modules)/**/}/*.spec.ts\" --reporter spec --require ts-node/register --require 'node_modules/reflect-metadata/Reflect.js' --exit",
kamilmysliwiec marked this conversation as resolved.
Show resolved Hide resolved
"test:docker:up": "docker-compose -f integration/docker-compose.yml up -d",
"test:docker:down": "docker-compose -f integration/docker-compose.yml down",
"lint": "concurrently 'npm run lint:packages' 'npm run lint:integration' 'npm run lint:spec'",
Expand Down
56 changes: 37 additions & 19 deletions packages/common/test/utils/shared.utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
isPlainObject,
isString,
isUndefined,
normalizePath,
} from '../../utils/shared.utils';

function Foo(a) {
Expand All @@ -17,34 +18,34 @@ function Foo(a) {

describe('Shared utils', () => {
describe('isUndefined', () => {
it('should returns true when obj is undefined', () => {
it('should return true when obj is undefined', () => {
expect(isUndefined(undefined)).to.be.true;
});
it('should returns false when object is not undefined', () => {
it('should return false when object is not undefined', () => {
expect(isUndefined({})).to.be.false;
});
});
describe('isFunction', () => {
it('should returns true when obj is function', () => {
it('should return true when obj is function', () => {
expect(isFunction(() => ({}))).to.be.true;
});
it('should returns false when object is not function', () => {
it('should return false when object is not function', () => {
expect(isFunction(null)).to.be.false;
expect(isFunction(undefined)).to.be.false;
});
});
describe('isObject', () => {
it('should returns true when obj is object', () => {
it('should return true when obj is object', () => {
expect(isObject({})).to.be.true;
});
it('should returns false when object is not object', () => {
it('should return false when object is not object', () => {
expect(isObject(3)).to.be.false;
expect(isObject(null)).to.be.false;
expect(isObject(undefined)).to.be.false;
});
});
describe('isPlainObject', () => {
it('should returns true when obj is plain object', () => {
it('should return true when obj is plain object', () => {
expect(isPlainObject({})).to.be.true;
expect(isPlainObject({ prop: true })).to.be.true;
expect(
Expand All @@ -54,7 +55,7 @@ describe('Shared utils', () => {
).to.be.true;
expect(isPlainObject(Object.create(null))).to.be.true;
});
it('should returns false when object is not object', () => {
it('should return false when object is not object', () => {
expect(isPlainObject(3)).to.be.false;
expect(isPlainObject(null)).to.be.false;
expect(isPlainObject(undefined)).to.be.false;
Expand All @@ -64,52 +65,69 @@ describe('Shared utils', () => {
});
});
describe('isString', () => {
it('should returns true when obj is string', () => {
it('should return true when obj is a string', () => {
expect(isString('true')).to.be.true;
});
it('should returns false when object is not string', () => {
it('should return false when object is not a string', () => {
expect(isString(false)).to.be.false;
expect(isString(null)).to.be.false;
expect(isString(undefined)).to.be.false;
});
});
describe('isConstructor', () => {
it('should returns true when string is equal constructor', () => {
it('should returntrue when string is equal to constructor', () => {
kamilmysliwiec marked this conversation as resolved.
Show resolved Hide resolved
expect(isConstructor('constructor')).to.be.true;
});
it('should returns false when string is not equal constructor', () => {
it('should return false when string is not equal to constructor', () => {
expect(isConstructor('nope')).to.be.false;
});
});
describe('addLeadingSlash', () => {
it('should returns validated path ("add / if not exists")', () => {
it('should return the validated path ("add / if not exists")', () => {
expect(addLeadingSlash('nope')).to.be.eql('/nope');
});
it('should returns same path', () => {
it('should return the same path', () => {
expect(addLeadingSlash('/nope')).to.be.eql('/nope');
});
it('should returns empty path', () => {
it('should return empty path', () => {
expect(addLeadingSlash('')).to.be.eql('');
expect(addLeadingSlash(null)).to.be.eql('');
expect(addLeadingSlash(undefined)).to.be.eql('');
});
});
describe('normalizePath', () => {
it('should remove all trailing slashes at the end of the path', () => {
expect(normalizePath('path/')).to.be.eql('/path');
expect(normalizePath('path///')).to.be.eql('/path');
expect(normalizePath('/path/path///')).to.be.eql('/path/path');
});
it('should replace all slashes with only one slash', () => {
expect(normalizePath('////path/')).to.be.eql('/path');
expect(normalizePath('///')).to.be.eql('/');
expect(normalizePath('/path////path///')).to.be.eql('/path/path');
});
it('should return / for empty path', () => {
expect(normalizePath('')).to.be.eql('/');
expect(normalizePath(null)).to.be.eql('/');
expect(normalizePath(undefined)).to.be.eql('/');
});
});
describe('isNil', () => {
it('should returns true when obj is undefined or null', () => {
it('should return true when obj is undefined or null', () => {
expect(isNil(undefined)).to.be.true;
expect(isNil(null)).to.be.true;
});
it('should returns false when object is not undefined and null', () => {
it('should return false when object is not undefined and null', () => {
expect(isNil('3')).to.be.false;
});
});
describe('isEmpty', () => {
it('should returns true when array is empty or not exists', () => {
it('should return true when array is empty or not exists', () => {
expect(isEmpty([])).to.be.true;
expect(isEmpty(null)).to.be.true;
expect(isEmpty(undefined)).to.be.true;
});
it('should returns false when array is not empty', () => {
it('should return false when array is not empty', () => {
expect(isEmpty([1, 2])).to.be.false;
});
});
Expand Down
7 changes: 7 additions & 0 deletions packages/common/utils/shared.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ export const addLeadingSlash = (path?: string): string =>
*/
export const validatePath = addLeadingSlash;

export const normalizePath = (path?: string): string =>
path
? path.startsWith('/')
? ('/' + path.replace(/\/+$/, '')).replace(/\/+/g, '/')
: '/' + path.replace(/\/+$/, '')
: '/';

export const isFunction = (fn: any): boolean => typeof fn === 'function';
export const isString = (fn: any): fn is string => typeof fn === 'string';
export const isConstructor = (fn: any): boolean => fn === 'constructor';
Expand Down
2 changes: 1 addition & 1 deletion packages/core/injector/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export class NestContainer {
scope: Type<any>[],
): Promise<Module> {
// In DependenciesScanner#scanForModules we already check for undefined or invalid modules
// We sill need to catch the edge-case of `forwardRef(() => undefined)`
// We still need to catch the edge-case of `forwardRef(() => undefined)`
if (!metatype) {
throw new UndefinedForwardRefException(scope);
}
Expand Down
9 changes: 8 additions & 1 deletion packages/core/injector/modules-container.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
import { v4 as uuid } from 'uuid';
import { Module } from './module';

export class ModulesContainer extends Map<string, Module> {}
export class ModulesContainer extends Map<string, Module> {
private readonly _applicationId = uuid();

get applicationId(): string {
return this._applicationId;
}
}
4 changes: 3 additions & 1 deletion packages/core/middleware/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,18 @@ import {
RouteInfo,
} from '@nestjs/common/interfaces/middleware';
import { MiddlewareConfiguration } from '@nestjs/common/interfaces/middleware/middleware-configuration.interface';
import { iterate } from 'iterare';
import { NestContainer } from '../injector';
import { RoutesMapper } from './routes-mapper';
import { filterMiddleware } from './utils';
import { iterate } from 'iterare';

export class MiddlewareBuilder implements MiddlewareConsumer {
private readonly middlewareCollection = new Set<MiddlewareConfiguration>();

constructor(
private readonly routesMapper: RoutesMapper,
private readonly httpAdapter: HttpServer,
private readonly container: NestContainer,
) {}

public apply(
Expand Down
Loading