-
Notifications
You must be signed in to change notification settings - Fork 1
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A couple very minor items, but otherwise LGTM.
I"ll mark it as approved so you don't need to wait for me to dismiss, but I do think there should be changes to the items mentioned.
src/logger-mixin/mixins/README.md
Outdated
|
||
### LoggerMixin | ||
|
||
LoggerMixin adds capabilities to a LoopBack Next Application by adding a `.logger()` function that allows used to bind a Logger class to `Context` automatically. The binding key will be `loggers.${Class.name}` where `Class.name` is the name of the Logger class being bound. The Mixin also overrides existing `.component()` function so that components are also capable of providing Logger's to be bound automatically. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
...function that allows used to...
--> that is used to
or that allows a
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
...also capable of providing Logger's to be...
--> also capable of providing their own Logger implementations automatically
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, split this line into multiple lines, it's far too long.
src/logger-mixin/mixins/README.md
Outdated
|
||
Once a Logger has been bound, you can retrieve it by using [Dependency Inject](http://loopback.io/doc/en/lb4/Dependency-injection.html) | ||
|
||
**More Examples for binding a Logger** |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#### More Logger Examples
* class MyApplication extends LoggerMixin(Application) {} | ||
* ``` | ||
*/ | ||
export function LoggerMixin<T extends Constructor<any>>(superClass: T) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this is meant to be used with Applications (or implementations of Application), then it should be of type Constructor<Application>
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 for using a more specific type than any
.
I don't think we can use Application
here, because we may be referring to a different constructor class (in our copy of @loopback/core
in node_modules
tree) than the constructor class used by the application using our mixin (from their copy of @loopback/core
in their node_modules
tree).
Considering that our own @loopback/repository
is using T extends Constructor<any>
(source), I think it's ok to do the same here, at least for now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using type Application throws a whole series of errors such as using internal type ApplicationOptions
, etc. I think it's because we don't have an interface for Application
. Even setting it to `Constructor<{}> results in errors.
if (this.options.components) { | ||
// Super would have already mounted the component | ||
for (const component of this.options.components) { | ||
this.component(component); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this.component
is already called in the base Application constructor, which means you should be able to do this:
if (this.options.components) {
for (const component of this.options.components) {
mountComponentLogger(component);
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not needed at all. For the mixin to work correctly, it's enough to override component
method (as is already done below).
Application
's constructor, invoked via super(...args)
at the top of this method, will call this.component
, there is no need to call it explicitly again. Unless I am missing something about how the prototypal inheritance and class
keyword works in JavaScript?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using type Application throws a whole series of errors such as using internal type ApplicationOptions
, etc. I think it's because we don't have an interface for Application
. Even setting it to `Constructor<{}> results in errors.
EDIT: This was meant for comment RE: Constructor
type being any
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Your pull request mixes changes in project infrastructure (how files are transpiled, etc.) with the code for mixin extension. Perhaps the infrastructure changes were accidental? Please remove the infrastructure changes from this pull request and send them in a standalone PR if they are intentional.
index.ts
Outdated
// This file is licensed under the MIT License. | ||
// License text available at https://opensource.org/licenses/MIT | ||
|
||
export * from './src'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the purpose of this file, why is it needed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm unable to complete a build without this file as it results in errors with exported members not being found. We have it in all loopback-next/packages/*
directories as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
package.json
Outdated
"lint": "tslint -c tslint.full.json --project tsconfig.json --type-check", | ||
"lint:fix": "npm run lint -- --fix", | ||
"prepublish": "npm run build", | ||
"pretest": "npm run build", | ||
"pretest": "npm run clean && npm run build", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it a good idea to run clean
task before each test run? What if npm run build
was able to do an incremental rebuild?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The issue I ran into with the build was that if I had a file somefile.ts
and had done a build, then if I deleted that file, added a new file somenewfile.ts
and then ran build again (without clean) then the dist folder would have both files in it.
Reference Issue: microsoft/TypeScript#13722
src/controllers/README.md
Outdated
@@ -1,3 +0,0 @@ | |||
# Controllers | |||
|
|||
This directory contains source files for the controllers exported by this extension. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you preserve this file please? Without it, the entire controllers
directory will be removed by git. Unless that's what you are trying to accomplish?
package.json
Outdated
@@ -43,6 +41,7 @@ | |||
"@loopback/testlab": "^4.0.0-alpha.7", | |||
"@types/mocha": "^2.2.43", | |||
"mocha": "^3.5.3", | |||
"ts-node": "^3.3.0", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As of #2, we are not using ts-node
. We are transpiling both production and test code in one go. In the development workflow, developer should start tsc
in watch mode in background - that way files in dist
are always up to date with the typescript sources. The tests are run via regular mocha
then.
The project setup works well for people using VS Code. IIRC, you mentioned @virkt25 that you are using a different editor/IDE. To support your preference, we should add a new npm script for running tsc
in watch mode, e.g. "tsc-watch": "tsc --target es2017 --outDir dist --watch"
. Then the instructions become:
- Start the build process in background via
npm run tsc-watch
. It will run TypeScript compiler in backround, watching and recompiling files as you change them. - Run
npm run vscode-test
to re-run the test suite and lint the code for both programming and style errors.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I created a pull request to describe dev setup for people not using VS Code - see #8.
src/logger-mixin/index.ts
Outdated
// This file is licensed under the MIT License. | ||
// License text available at https://opensource.org/licenses/MIT | ||
|
||
export * from './mixins/logger-mixin'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In #2, I laid out a directory structure where we have directories for different entities (bindings, mixins, repositories, controllers) at the first level, and then individual source files are nested in these directories. This is a convention I'd like to promote for all kinds of LoopBack-Next projects - loopback-next monorepo packages, extension packages, applications.
In that layout, your mixin file should go to src/mixins/logger-mixin.ts
.
Here is how I envision the usage: as a developer wishing to write a new extension that provides a mixin (plus few more things like sequence action bindings), I'll rename logger-mixin.ts
to a different name matching my extension intent (e.g. grpc-mixin.ts
), do the same for other templates/examples I'd like to leverage (e.g. rename the template/example of a binding from. foobar-provider.ts
to invoke-method-provider.ts
, etc.) and have a working component.
What I'd personally like to avoid is the necessity to move files and directories around, e.g. move src/logger-mixin/mixins/logger-mixin.ts' to
src/mixins/grpc-mixin.ts, because it involves too many changes in other places (various
index.ts` files, related test files, etc.).
if (this.options.components) { | ||
// Super would have already mounted the component | ||
for (const component of this.options.components) { | ||
this.component(component); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not needed at all. For the mixin to work correctly, it's enough to override component
method (as is already done below).
Application
's constructor, invoked via super(...args)
at the top of this method, will call this.component
, there is no need to call it explicitly again. Unless I am missing something about how the prototypal inheritance and class
keyword works in JavaScript?
test/mocha.opts
Outdated
--recursive | ||
--reporter dot | ||
dist/test |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please revert changes in this file, they are breaking the setup introduced by #2.
test/unit/logger-mixin.test.ts
Outdated
}); | ||
|
||
expectComponentToBeBound(myApp, EmptyTestComponent); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I find this test extraneous - the fact that Application's constructor invokes app.component()
is already tested in @loopback/core
. No need to test it again here.
test/unit/logger-mixin.test.ts
Outdated
}); | ||
|
||
const logger = myApp.getSync('loggers.MyLogger'); | ||
expect(typeof logger.log).to.be.eql('function'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's better to verify that the logger is an instance of MyLogger?
tsconfig.json
Outdated
@@ -9,13 +9,10 @@ | |||
"lib": ["es2017", "dom"], | |||
"module": "commonjs", | |||
"moduleResolution": "node", | |||
"target": "es2017", | |||
"target": "es6", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please revert.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While I have reverted the reason for this change was that package.json
says it supports node >= 6
... using es2017
means we can't support Node v6 as async
& await
doesn't work with target set to es2017
. I'd say we move to support only node >=8
(which will be the official support target for LBNext).
Will remove `package.json` once #8 has landed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The changes look much better now.
.vscode/tasks.json
Outdated
"command": "tsc", | ||
"args": ["--watch"], | ||
"command": "npm", | ||
"args": ["--silent", "run", "build:watch"], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I find this change suspicious, I hope it will go away after you rebase this patch on the latest master (that will include #10).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it should. will be rebasing shortly.
src/mixins/README.md
Outdated
|
||
## Overview | ||
|
||
A mixin is a class which take the input of a base class and adds / modifies existing functions / status values and returns a new class which can be instantiated. The type of the returned class will still be the base class. Mixins let you extend the BaseClass by adding more functions to it, overriding existing ones or modifiying their behavior. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am concerned that the content in this README is missing a clear explanation why extension developers would want/need to write an application mixin, and what are the necessary steps/features that such mixin should do in order to be consistent with other similar mixins. I would like to see a short paragraph explaining the different ways how a configuration can be provided to a LoopBack app (application config, component config, app.logger()
helper) and perhaps explaining how to implement each option via the mixin.
On the second thought, I think such content should be in http://loopback.io/doc/en/lb4/Creating-components.html and this README should link to it.
The type of the returned class will still be the base class
This is not true, the returned class will be a child class inheriting from the original base class.
src/mixins/README.md
Outdated
|
||
**Example** | ||
``` | ||
cosnt newClass = MixinClass(BaseClass); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo, should be const
I think we should be recommending class
-based approach in our examples.
class MyApp extends MixinClass(Application) {
}
One of the downsides of your current example is that we have a class constructor stored in a variable with camelCase name, when the convention for class constructors is to use PascalCase names.
(Same comments apply to the example below.)
src/mixins/README.md
Outdated
error(...args: any[]) { | ||
const data = args.join(' '); | ||
console.log('\x1b[31m error: ' + data + '\x1b[0m'); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In order to keep these examples shorter, I am proposing to reduce the number of logger methods to two: log
and error
. (This will require changes in the source code too.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code change will only apply to test cases as the mixin doesn't supply a Logger implementation.
src/mixins/logger.mixin.ts
Outdated
* | ||
* @param component The component to mount Logger's of | ||
*/ | ||
mountComponentLogger(component: Constructor<any>) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this is mounting possibly multiple logger, I think mountComponentLoggers
is a more appropriate name.
src/repositories/README.md
Outdated
@@ -1,3 +0,0 @@ | |||
# Repositories | |||
|
|||
This directory contains code for repositories provided by this extension. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we preserve this file please?
test/unit/logger-mixin.test.ts
Outdated
import {Application, Component} from '@loopback/core'; | ||
import {Constructor} from '@loopback/context'; | ||
|
||
describe('LoggerMixin', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please move this file to test/unit/mixins/logger.mixin.test.ts
. Related discussions: #6 (comment) and #6 (comment)
test/unit/logger-mixin.test.ts
Outdated
|
||
class MyLogger { | ||
// tslint:disable-next-line:no-any | ||
log(...args: any[]) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In order to avoid those tslint exceptions, I am proposing to define a type alias:
// tslint:disable-next-line:no-any
type LogArgs = any[];
// usage
log(...args: LogArgs) {
// ...
}
test/unit/logger-mixin.test.ts
Outdated
|
||
class AppWithLogger extends LoggerMixin(Application) {} | ||
|
||
class MyLogger { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In a real world extension, I would expect an interface defining the expected logger API - this interface should be provided as part of the extension. A test implementation of a logger should be marked as implementing this interface. As a result, when the API contract of a logger changes, tsc
will report errors for all test logger that don't match the new API yet.
tslint.full.json
Outdated
"extends": [ | ||
"./tslint.json" | ||
], | ||
// This configuration files enabled rules which require type checking |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The changes in this file look like pure whitespace and formatting updates. Can you revert them please, and perhaps send in a new PR?
Will remove `package.json` once #8 has landed.
src/mixins/README.md
Outdated
```ts | ||
constructor(...args: any[]) { | ||
super(args); | ||
}``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo: triple back-tick must be on a standalone line.
src/mixins/README.md
Outdated
### High level example | ||
```ts | ||
const MixedClass = MyMixinClass(Application); | ||
const MultipleMixedClassesClass = MyMixinClass(MyMixinClass2(Application)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you please change one of these examples to show how a mixin can be used with class inheritance? E.g.
class MixedApp extends MyMixinClass(Application) {
}
As I commented earlier, I think this is the usage we should be promoting, and it may not be obvious to all TypeScript developers that mixins can be invoked inside extends
clause.
src/mixins/README.md
Outdated
|
||
#### Example user experience | ||
```ts | ||
new LoggerMixin(Application)({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, this is not how most people are going to use a mixin. Please save the new app to a variable (or create a new class extending the mixed-in app) and then call new
on this new variable/class.
src/mixins/README.md
Outdated
loggers: [ColorLogger]; | ||
} | ||
|
||
const LoggingApplication = LoggerMixin(Application); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LoggingApplication
has been already defined on line 109, I think this definition is redundant and should be removed.
src/mixins/README.md
Outdated
const app = this; | ||
} | ||
|
||
app.logger(ColorLogger); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This line should be moved outside of class LoggingApplication
. The app
variable is also not defined.
Did you perhaps want to call this.logger(ColorLogger)
from the constructor?
src/types.ts
Outdated
error(...args: LogArgs): void; | ||
|
||
info?(...args: LogArgs): void; | ||
warn?(...args: LogArgs): void; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I find this logger interface with optional methods difficult to use. What is my code supposed to do when the logger instance I receive does not provide info
method? Call log
? And what if warn
is missing, how should I log warnings?
If you feel that a logger class without info & warn is not complete, then please replace these two optional methods with a comment explaining that a typical logger would have more methods, but we are omitting them in order to keep this example/template smaller.
Will remove `package.json` once #8 has landed.
Will remove `package.json` once #8 has landed.
1499538
to
b1f867e
Compare
Initial implementation for a logger mixin that allows applications to bind a Logger automatically.
@bajtos feedback was applied. PTAL. |
src/mixins/README.md
Outdated
error(...args: LogArgs) { | ||
const data = args.join(' '); | ||
// log in red color | ||
console.log('\x1b[31m error: ' + data + '\x1b[0m'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe console.log('\x1b[31m error:', ...args, '\x1b[0m')
for consistency with the log
method? args.join(' ')
will convert object values to string via .toString()
, console.log
uses util.format
instead.
src/mixins/README.md
Outdated
|
||
|
||
## Examples for using LoggerMixin | ||
``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add ts
suffix to enable syntax highlighting.
src/mixins/README.md
Outdated
class LoggingApplication extends LoggerMixin(Application) { | ||
constructor(...args: any[]) { | ||
super(...args); | ||
this.logger(ColorLogger); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is not necessary, because LoggingComponent
is already providing ColorLogger
. Can we remove this line please? Am I missing something?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess I wasn't clear but I was trying to demonstrate the various ways an end user would use LoggerMixin
. I've updated the examples to be distinct so hopefully that clears that up.
LMK your thoughts on including this section.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
|
||
describe('logger.mixin (acceptance)', () => { | ||
// tslint:disable-next-line:no-any | ||
let app: any; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When app
is any, IDEs are not able to offer any meaningful autocompletion and the compiler cannot verify correct usage. I am proposing to move the definition of LoggerApplication
out of createApp
so that we can use it here.
A simpler alternative (wich may not work, IDK), is to use LoggerMixin(Application)
as the type of this variable.
let app: LoggerMixin(Application);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This unfortunately doesn't work. I even tried using typeof
on this as well as <>
. I think any works well here because multiple mixins can be combined and it becomes difficult to nail down the final shape of the Mixin.
An interesting thing to note however is that we can use the LoggerApplication
as the type if the class is moved outside of the createApplication
function which is what I've done & think this is the approach most users will take as well.
I did however come across this while doing some research: microsoft/TypeScript#12114
I haven't tried any of the generic mappings and from reading I'm not sure it's possible to create a generic enough type for multiple mixins applied to a class.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An interesting thing to note however is that we can use the LoggerApplication as the type if the class is moved outside of the createApplication function which is what I've done & think this is the approach most users will take as well.
👍
let app: any; | ||
let server: RestServer; | ||
// tslint:disable-next-line:no-any | ||
let spy: any; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am pretty sure sinon provides a type that can be used for spies, see e.g. how we are using sinon.SinonStub
in Thinking in LoopBack.
let spy: sinon.SinonSpy;
beforeEach(createLogger); | ||
beforeEach(createController); | ||
beforeEach(getServerFromApp); | ||
beforeEach(() => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We try to use named hooks, see http://loopback.io/doc/en/contrib/style-guide.html#hooks
spy = sinon.spy(console, 'log'); | ||
}); | ||
|
||
afterEach(() => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ditto.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lovely,
src/mixins/README.md
Outdated
class LoggingApplication extends LoggerMixin(Application) { | ||
constructor(...args: any[]) { | ||
super(...args); | ||
this.logger(ColorLogger); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
A starter mixin extension that implements a simple
.logger()
function to bind aLogger
automatically tologger.
prefix.connect to loopbackio/loopback-next#525