Git Product home page Git Product logo

fold's Introduction

AdonisJS Fold

Simplest, straightforward implementation for IoC container in JavaScript


gh-workflow-image npm-image license-image

Why this project exists?

Many existing implementations of IoC containers take the concept too far and start to feel more like Java. JavaScript inherently does not have all the bells and whistles; you need to have similar IoC container benefits as PHP or Java.

Therefore, with this project, I live to the ethos of JavaScript and yet build a container that can help you create loosely coupled systems.

I have explained the reasons for using an IoC container in this post. It might be a great idea to read the post first ✌️

Note: AdonisJS fold is highly inspired by the Laravel IoC container. Thanks to Taylor for imaginging such a simple, yet powerful API.

Goals of the project

  • Keep the code visually pleasing. If you have used any other implementation of an IoC container, you will automatically find @adonisjs/fold easy to read and follow.
  • Keep it simple. JavaScript projects have a few reasons for using an IoC container, so do not build features that no one will ever use or understand.
  • Build it for JavaScript and improve with TypeScript - The implementation of @adonisjs/fold works with vanilla JavaScript. It's just you have to write less code when using TypeScript. Thanks to its decorators metadata API.

Usage

Install the package from the npm packages registry.

npm i @adonisjs/fold@next

# yarn lovers
yarn add @adonisjs/fold@next

# pnpm followers
pnpm add @adonisjs/fold@next

Once done, you can import the Container class from the package and create an instance of it. For the most part, you will use a single instance of the container.

import { Container } from '@adonisjs/fold'

const container = new Container()

Making classes

You can construct an instance of a class by calling the container.make method. The method is asynchronous since it allows for lazy loading dependencies via factory functions (More on factory functions later).

class UserService {}

const service = await container.make(UserService)
assert(service instanceof UserService)

In the previous example, the UserService did not have any dependencies; therefore, it was straightforward for the container to make an instance of it.

Now, let's look at an example where the UserService needs an instance of the Database class.

class Database {}

class UserService {
  static containerInjections = {
    _constructor: {
      dependencies: [Database],
    },
  }

  constructor(db) {
    this.db = db
  }
}

const service = await container.make(UserService)
assert(service.db instanceof Database)

The static containerInjections property is required by the container to know which values to inject when creating an instance of the class.

This property can define the dependencies for the class methods (including the constructor). The dependencies are defined as an array. The dependencies are injected in the same order as they are defined inside the array.

Do you remember? I said that JavaScript is not as powerful as Java or PHP. This is a classic example of that. In other languages, you can use reflection to look up the classes to inject, whereas, in JavaScript, you have to tell the container explicitly.

TypeScript to the rescue

Wait, you can use decorators with combination of TypeScript's emitDecoratorMetaData option to perform reflection. You will also need to install reflect-metadata in order for TypeScript to extract metadata from your classes.

It is worth noting, TypeScript decorators are not as powerful as the reflection API in other languages. For example, in PHP, you can use interfaces for reflection. Whereas in TypeScript, you cannot.

With that said, let's look at the previous example, but in TypeScript this time.

import { inject } from '@adonisjs/fold'

class Database {}

@inject()
class UserService {
  constructor(db: Database) {
    this.db = db
  }
}

const service = await container.make(UserService)
assert(service.db instanceof Database)

The @inject decorator looks at the types of all the constructor parameters and defines the static containerInjections property behind the scenes.

Note: The decorator-based reflection can only work with concrete values, not with interfaces or types since they are removed during the runtime.

Making class with runtime values

When calling the container.make method, you can pass runtime values that take precedence over the containerInjections array.

In the following example, the UserService accepts an instance of the ongoing HTTP request as the 2nd param. Now, when making an instance of this class, you can pass that instance manually.

import { inject } from '@adonisjs/fold'
import { Request } from '@adonisjs/core/src/Request'

class Database {}

@inject()
class UserService {
  constructor(db: Database, request: Request) {
    this.db = db
    this.request = request
  }
}
createServer((req) => {
  const runtimeValues = [undefined, req]

  const service = await container.make(UserService, runtimeValues)
  assert(service.request === req)
})

In the above example:

  • The container will create an instance of the Database class since it is set to undefined inside the runtime values array.
  • However, for the second position (ie request), the container will use the req value.

Calling methods

You can also call class methods to look up/inject dependencies automatically.

In the following example, the UserService.find method needs an instance of the Database class. The container.call method will look at the containerInjections property to find the values to inject.

class Database {}

class UserService {
  static containerInjections = {
    find: {
      dependencies: [Database],
    },
  }

  async find(db) {
    await db.select('*').from('users')
  }
}

const service = await container.make(UserService)
await container.call(service, 'find')

The TypeScript projects can re-use the same @inject decorator.

class Database {}

class UserService {
  @inject()
  async find(db: Database) {
    await db.select('*').from('users')
  }
}

const service = await container.make(UserService)
await container.call(service, 'find')

The runtime values are also supported with the container.call method.

Container bindings

Alongside making class instances, you can also register bindings inside the container. Bindings are simple key-value pairs.

  • The key can either be a string, a symbol or a class constructor.
  • The value is a factory function invoked when someone resolves the binding from the container.
const container = new Container()

container.bind('db', () => {
  return new Database()
})

const db = await container.make('db')
assert(db instanceof Database)

Following is an example of binding the class constructor to the container and self constructing an instance of it using the factory function.

container.bind(Database, () => {
  return new Database()
})

Factory function arguments

The factory receives the following three arguments.

  • The resolver reference. Resolver is something container uses under the hood to resolve dependencies. The same instance is passed to the factory, so that you can resolve dependencies to construct the class.
  • An optional array of runtime values defined during the container.make call.
container.bind(Database, (resolver, runtimeValues) => {
  return new Database()
})

When to use the factory functions?

I am answering this question from a framework creator perspective. I never use the @inject decorator on my classes shipped as packages. Instead, I define their construction logic using factory functions and keep classes free from any knowledge of the container.

So, if you create packages for AdonisJS, I highly recommend using factory functions. Leave the @inject decorator for the end user.

Binding singletons

You can bind a singleton to the container using the container.singleton method. It is the same as the container.bind method, except the factory function is called only once, and the return value is cached forever.

container.singleton(Database, () => {
  return new Database()
})

Binding values

Along side the factory functions, you can also bind direct values to the container.

container.bindValue('router', router)

The values are given priority over the factory functions. So, if you register a value with the same name as the factory function binding, the value will be resolved from the container.

The values can also be registered at the resolver level. In the following example, the Request binding only exists for an isolated instance of the resolver and not for the entire container.

const resolver = container.createResolver()
resolver.bindValue(Request, req)

await resolve.make(SomeClass)

Aliases

Container aliases allows defining aliases for an existing binding. The alias should be either a string or a symbol.

container.singleton(Database, () => {
  return new Database()
})

container.alias('db', Database)

/**
 * Make using the alias
 */
const db = await container.make('db')
assert.instanceOf(db, Database)

Contextual bindings

Contextual bindings allows you to register custom dependency resolvers on a given class for a specific dependency. You will be mostly using contextual bindings with driver based implementations.

For example: You have a UserService and a BlogService and both of them needs an instance of the Drive disk to write and read files. You want the UserService to use the local disk driver and BlogService to use the s3 disk driver.

Note Contextual bindings can be defined for class constructors and not for container bindngs

import { Disk } from '@adonisjs/core/driver'

class UserService {
  constructor(disk: Disk) {}
}
import { Disk } from '@adonisjs/core/driver'

class BlogService {
  constructor(disk: Disk) {}
}

Now, let's use contextual bindings to tell the container that when UserService needs the Disk class, provide it the local driver disk.

container
  .when(BlogService)
  .asksFor(Disk)
  .provide(() => drive.use('s3'))

container
  .when(UserService)
  .asksFor(Disk)
  .provide(() => drive.use('local'))

Swapping implementations

When using the container to resolve a tree of dependencies, quite often you will have no control over the construction of a class and therefore you will be not able to swap/fake its dependencies when writing tests.

In the following example, the UsersController needs an instance of the UserService class.

@inject()
class UsersController {
  constructor(service: UserService) {}
}

In the following test, we are making an HTTP request that will be handled by the UsersController. However, within the test, we have no control over the construction of the controller class.

test('get all users', async ({ client }) => {
  // I WANTED TO FAKE USER SERVICE FIRST?
  const response = await client.get('users')
})

To make things simpler, you can tell the container to use a swapped implementation for a given class constructor as follows.

test('get all users', async ({ client }) => {
  class MyFakedService extends UserService {}

  /**
   * From now on, the container will return an instance
   * of `MyFakedService`.
   */
  container.swap(UserService, () => new MyFakedService())

  const response = await client.get('users')
})

Observing container

You can pass an instance of the EventEmitter or emittery to listen for events as container resolves dependencies.

import { EventEmitter } from 'node:events'
const emitter = new EventEmitter()

emitter.on('container:resolved', ({ value, binding }) => {
  // value is the resolved value
  // binding name can be a mix of string, class constructor, or a symbol.
})

const container = new Container({ emitter })

Container hooks

You can use container hooks when you want to modify a resolved value before it is returned from the make method.

  • The hook is called everytime a binding is resolved from the container.
  • It is called only once for the singleton bindings.
  • The hook gets called everytime you construct an instance of a class by passing the class constructor directly.

Note: The hook callback can also be an async function

container.resolving(Validator, (validator) => {
  validate.rule('email', function () {})
})

Container providers

Container providers are static functions that can live on a class to resolve the dependencies for the class constructor or a given class method.

Once, you define the containerProvider on the class, the IoC container will rely on it for resolving dependencies and will not use the default provider.

import { ContainerResolver } from '@adonisjs/fold'
import { ContainerProvider } from '@adonisjs/fold/types'

class UsersController {
  static containerProvider: ContainerProvider = (
    binding,
    property,
    resolver,
    defaultProvider,
    runtimeValues
  ) => {
    console.log(binding === UserService)
    console.log(this === UserService)
    return defaultProvider(binding, property, resolver, runtimeValues)
  }
}

Why would I use custom providers?

Custom providers can be handy when creating an instance of the class is not enough to construct it properly.

Let's take an example of AdonisJS route model binding. With route model binding, you can query the database using models based on the value of a route parameter and inject the model instance inside the controller.

import User from '#models/User'
import { bind } from '@adonisjs/route-model-binding'

class UsersController {
  @bind()
  public show(_, user: User) {}
}

Now, if you use the @inject decorator to resolve the User model, then the container will only create an instance of User and give it back to you.

However, in this case, we want more than just creating an instance of the model. We want to look up the database and create an instance with the row values.

This is where the @bind decorator comes into the picture. To perform database lookups, it registers a custom provider on the UsersController class.

Binding types

If you are using the container inside a TypeScript project, then you can define the types for all the bindings in advance at the time of creating the container instance.

Defining types will ensure the bind, singleton and bindValue method accepts only the known bindings and assert their types as well.

class Route {}
class Databse {}

type ContainerBindings = {
  route: Route
  db: Database
}

const container = new Container<ContainerBindings>()

// Fully typed
container.bind('route', () => new Route())
container.bind('db', () => new Database())

// Fully typed - db: Database
const db = await container.make('db')

Common errors

Cannot inject "xxxxx" in "[class: xxxxx]". The value cannot be constructed

The error occurs, when you are trying to inject a value that cannot be constructed. A common source of issue is within TypeScript project, when using an interface or a type for dependency injection.

In the following example, the User is a TypeScript type and there is no way for the container to construct a runtime value from this type (types are removed after transpiling the TypeScript code).

Therefore, the container will raise an exception saying Cannot inject "[Function: Object]" in "[class: UsersController]". The value cannot be constructed.

type User = {
  username: string
  age: number
  email: string
}

@inject()
class UsersController {
  constructor(user: User)
}

Module expressions

In AdonisJS, we allow binding methods in form of string based module expression. For example:

Instead of importing and using a controller as follows

import UsersController from '#controllers/users'

Route.get('users', (ctx) => {
  return new UsersController().index(ctx)
})

You can bind the controller method as follows

Route.get('users', '#controllers/users.index')

Why do we do this? There are a couple of reasons for using module expressions.

  • Performance: Lazy loading controllers ensures that we keep the application boot process quick. Otherwise, we will pull all the controllers and their imports within a single routes file and that will surely impact the boot time of the application.
  • Visual clutter: Imagine importing all the controllers within a single routes file (or maybe 2-3 different route files) and then instantiating them manually. This surely brings some visual clutter in your codebase (agree visual clutter is subjective).

So given we use module expressions widely in the AdonisJS ecosystem. We have abstracted the logic of parsing string based expressions into dedicated helpers to re-use and ease.

Assumptions

There is a strong assumption that every module references using module expression will have a export default exporting a class.

// Valid module for module expression
export default class UsersController {}

toCallable

The toCallable method returns a function that internally parses the module string expression and returns a function that you can invoke like any other JavaScript function.

import { moduleExpression } from '@adonisjs/fold'

const fn = moduleExpression('#controllers/users.index', import.meta.url).toCallable()

// Later call it
const container = new Container()
const resolver = container.createResolver()
await fn(resolver, [ctx])

You can also pass the container instance at the time of creating the callable function.

const container = new Container()
const fn = moduleExpression('#controllers/users.index', import.meta.url).toCallable(container)

// Later call it
await fn([ctx])

toHandleMethod

The toHandleMethod method returns an object with the handle method. To the main difference between toCallable and toHandleMethod is their return output

  • toHandleMethod returns { handle: fn }
  • toCallable returns fn
import { moduleExpression } from '@adonisjs/fold'

const handler = moduleExpression('#controllers/users.index', import.meta.url).toHandleMethod()

// Later call it
const container = new Container()
const resolver = container.createResolver()
await handler.handle(resolver, [ctx])

You can also pass the container instance at the time of creating the handle method.

const container = new Container()

const handler = moduleExpression('#controllers/users.index', import.meta.url).toHandleMethod(
  container
)

// Later call it
await handler.handle([ctx])

Bechmarks

Following are benchmarks to see the performance loss that happens when using module expressions.

Benchmarks were performed on Apple M1 iMac, 16GB

  • handler: Calling the handle method on the output of toHandleMethod.
  • callable: Calling the function returned by the toCallable method.
  • native: Using dynamic imports to lazily import the module. This variation does not use any helpers from this package.
  • inline: When no lazy loading was performed. The module was importing inline using the import keyword.

Module importer

The module importer is similar to module expression. However, instead of defining the import path as a string, you have to define a function that imports the module.

import { moduleImporter } from '@adonisjs/fold'

const fn = moduleImporter(
  () => import('#middleware/auth')
  'handle'
).toCallable()

// Later call it
const container = new Container()
const resolver = container.createResolver()
await fn(resolver, [ctx])

Create handle method object

import { moduleImporter } from '@adonisjs/fold'

const handler = moduleImporter(
  () => import('#middleware/auth')
  'handle'
).toHandleMethod()

// Later call it
const container = new Container()
const resolver = container.createResolver()
await handler.handle(resolver, [ctx])

Module caller

The module caller is similar to module importer. However, instead of lazy loading a class, you pass the class constructor to this method.

import { moduleCaller } from '@adonisjs/fold'

class AuthMiddleware {
  handle() {}
}

const fn = moduleCaller(AuthMiddleware, 'handle').toCallable()

// Later call it
const container = new Container()
const resolver = container.createResolver()
await fn(resolver, [ctx])

Create handle method object

import { moduleCaller } from '@adonisjs/fold'

class AuthMiddleware {
  handle() {}
}

const handler = moduleImporter(AuthMiddleware, 'handle').toHandleMethod()

// Later call it
const container = new Container()
const resolver = container.createResolver()
await handler.handle(resolver, [ctx])

fold's People

Contributors

dependabot-preview[bot] avatar dependabot[bot] avatar julien-r44 avatar romainlanz avatar snyk-bot avatar targos avatar thetutlage avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

fold's Issues

Cannot Read property "_hasFake" of undefined

I am migrating from Adonis 3.2 to Adonis 4.0. While trying to connect using JWT auth token I received this error. I'm open to the idea of it being an operator error, but any enlightenment would be much appreciated at this point.

node_modules/@adonisjs/fold/src/Ioc/index.js:712:14
Cannot Read property "_hasFake" of undefined
screen shot 2018-02-01 at 1 04 50 pm

Add Typescript support

Why this feature is required (specific use-cases will be appreciated)?

When using Typescript in strict mode, the code will not compile because TS definitions are not present.

Have you tried any other work arounds?

I have tried looking up definitions in DefinitelyTyped repository but it's not there.

Are you willing to work on it with little guidance?

Maybe.

Not able to make a Thenable Class

Package version

@adonisjs/[email protected]

Describe the bug

This is a simple Thenable Class Example

class Database {
    where() {
        return this
    }
    and() {
        return this
    }
    limit() {
        return this
    }
    then(onFulfilled: (result: string) => any) {
        onFulfilled('my example result')
    }
}

What I expect is

const db = await app.container.make(Database)
const result = await db.where().and().limit()
result === 'my example result''

But the result is db === 'my example result', not a instance of Database.
The thenable class is useful for graceful chain methods

Reproduction repo

https://github.com/shiny/reproduce-thenable-class-issue/blob/main/tests/unit/example.spec.ts

Please publish package 4.0.9 in npm repository, because of last available has 3.0.3

In maser I can see

  "version": "4.0.9",

but on npm page

https://www.npmjs.com/package/adonis-fold

3.0.3 • Public • Published 2 years ago

Please update it. It is related with the following dependencies tree

├─┬ [email protected]
│ └─┬ [email protected]
│   └─┬ [email protected]
│     └─┬ [email protected]
│       └─┬ [email protected]
│         ├─┬ [email protected]
│         │ └── [email protected]  deduped
│         ├── [email protected] 
│         └─┬ [email protected]
│           └── [email protected]  deduped

But these old packages has problems with npm audit security report and now are updated.

In your master I can see you require

    "coveralls": "^3.0.2",

So only publication in npm is required to fix it.

ioc.make bug

ioc.make don't worked with classes without name and space after class (generated by babel or other).

don't worked:

// app/Foo.js
module.exports = class{}
// test.js
make('App/Foo')

worked correctly:

// app/Foo.js
module.exports = class {
}
// app/Bar.js
module.exports = class Bar {
}
// test.js
make('App/Foo')
make('App/Bar')

I think bug into regular expression of function isClass:

const isClass = (fn) => {
  return typeof (fn) === 'function' && /^class\s/.test(toString.call(fn))
}

Module.prototype instanceof ServiceProvider

Hello i'm receiving RuntimeException: E_INVALID_SERVICE_PROVIDER: PageProvider must extend base service provider class
with adonis 4 and node 8.9
upon start

this is my PageProvider.js

'use strict';

const { ServiceProvider } = require('@adonisjs/fold');

class PageProvider extends ServiceProvider {
	register() {
		this.app.singleton('Sparrow/Page', () => {
			const Config = this.app.use('Adonis/Src/Config');
			return new (require('../src'))(Config);
		});
	}

	boot() {
		const View = this.app.use('Adonis/Src/View');
		/**
		 * Rendering the current position
		 */
		View.global('position', function(modules, position) {
			if (modules && modules[position]) {
				return modules[position];
			}
			return '';
		});
	}
}

module.exports = PageProvider;

debugger took me here https://github.com/poppinss/adonis-fold/blob/develop/src/Registrar/index.js#L105

Module.prototype instanceof ServiceProvider returns false

Classes fetch by adonis-fold isn't reseted

Hey,

The instance of a class fetch by adonis-fold isn't reseted at every request (with bind or singleton method).

There's the output of my console after every request.

$ npm start

> [email protected] start /Users/romainlanz/workspace/adonis/event-provider
> node --harmony_proxies server.js

info adonis:framework serving app on 0.0.0.0:3333
[ 'test' ]
[ 'test', 'test' ]
[ 'test', 'test', 'test' ]
[ 'test', 'test', 'test', 'home' ]
[ 'test', 'test', 'test', 'home', 'test' ]

Provider

'use strict'

const ServiceProvider = require('adonis-fold').ServiceProvider

class SomeProvider extends ServiceProvider {

  * register() {
      this.app.singleton('SomeClass', function(app) {
        const SomeClass = require('./SomeClass')

        return new SomeClass
      })
  }

}

module.exports = SomeProvider

Instance

'use strict'

class SomeClass {

  constructor() {
    this.elements = []
  }

  add(element) {
    this.elements.push(element)
  }

  get() {
    return this.elements
  }

}

module.exports = SomeClass

Test code

'use strict'

const SomeClass = use('SomeClass')

class HomeController {

  * index (request, response) {
    const view = yield response.view('index')

    SomeClass.add('home')
    console.log(SomeClass.get())

    response.send(view)
  }

  * test (request, response) {
    const view = yield response.view('index')

    SomeClass.add('test')
    console.log(SomeClass.get())

    response.send(view)
  }

}

module.exports = HomeController

Cannot use with symbolic links/npm link

When developing services locally it would be useful to use the npm link command to create local npm modules.

Inside of the use method it's looking for a type (Ioc._type(namespace)) which is always being returned as undefined. It works fine if the service provider lives inside of the node_modules directory though.

Faked service is not being recognized in classes outside of test that load the dependency

Behavior

Hi, I'm attempting to mock a service in an Adonis test by using ioc.fake, as documented in the Adonis testing documentation, but the behavior I'm seeing is that the class is only mocked within my test, and it is not mocked within the other classes under test that rely on the fake for a dependency. I am importing the class as I typically would with a use() statement in the other classes, rather than passing in the mock as an argument to a class constructor. The documentation made it seem like that was the correct use case...? Am I supposed to instead create a mock within my test and then manually pass it in as a dependency to any class that relies on it?

I thought that maybe the issue was that I had to mock the service before I include other classes through use() that rely on the mocked service as a dependency, but when I tried that, I saw the same behavior.

I've included code below that hopefully will illustrate my point if this doesn't quite make sense yet. I added a comment that says //ISSUE that traces the path through to where I'm seeing the actual class method being called rather than the fake class method. I also added console log statements for debugging; when I run adonis test I see in real getCourse printed from the actual function rather than in fake getCourse which should be printed from the fake function call. Thank you!

Package version

Fold: 4.0.9
Adonis: 5.0.8
Vow: 1.0.17

Node.js and npm version

Node: 10.8.0
Npm: 6.2.0

Sample Code (to reproduce the issue)

example.spec.js:

'use strict'

const { ioc } = use('@adonisjs/fold');
const { afterEach, before, beforeEach, test, trait } = use('Test/Suite')('Assignment due notification');

const Course = use('App/Models/Course');

let course;

//wraps all DB transactions and rolls them back after test completes
trait('DatabaseTransactions');

beforeEach(async () => {

    ioc.fake('App/Classes/Canvas/CanvasApi', () => {
        return {
            async getCourse() {
                console.log('in fake getCourse');
                return {
                    'name': 'Fake test course',
                    'term': {name: '2018'},
                    'time_zone': 'America/New_York',
                    'workflow_state': 'available'
                };
            }
        }
    });

    //DEBUGGING HERE: the mock shows up correctly within this test function,
    //but it does not show up correctly in any other class that uses it.
    //I have tried mocking the class here before loading in any dependencies,
    //and that did not change the behavior (accounted as well for variable hoisting)
    console.log('test in beforeEach: ');
    const canvasApi = use('App/Classes/Canvas/CanvasApi');
    console.log(canvasApi);

    const courseId = '000';
    const contextId = 'fake-lti-context-id';
    course = new Course();
    //ISSUE in the function below
    await course.create(courseId, contextId);

    ioc.restore('App/Classes/Canvas/CanvasApi'); //restore so class can be re-mocked within individual tests
});

test('it should not send notifications if no assignments are upcoming', async ({ assert }) => {
    //tests will eventually go here
});

App/Models/Course.js:

'use strict'

const Model = use('Model');
const CanvasApi = use('App/Classes/Canvas/CanvasApi');
const Config = use('Config');

class Course extends Model {
    courseNotificationSettings() {
        return this.hasMany('App/Models/CourseNotificationSetting');
    }

    async create(courseId, contextId) {
        //ISSUE in the function below
        const canvasCourseData = await this.getCanvasCourseData(courseId, {includeTerm: true});

        this.context_id = contextId;
        this.canvas_course_id = courseId;
        this.canvas_course_name = canvasCourseData.name;
        this.canvas_course_term = canvasCourseData.term.name;
        this.canvas_course_time_zone = canvasCourseData.time_zone;
        this.canvas_course_workflow_state = canvasCourseData.workflow_state;
        this.enabled = 0;

        await this.save();

        return this;
    }

    async getCanvasCourseData(courseId, options) {
        const canvasApi = new CanvasApi();
        //ISSUE in the function below
        return await canvasApi.getCourse(courseId, options);
    }
}

module.exports = Course;

App/Classes/Canvas/CanvasApi.js:

'use strict';

const Env = use('Env');
const axios = require('axios');
const moment = require('moment');
const parseLinkHeader = require('parse-link-header');

class CanvasApi {
    constructor() {
        this.apiToken = Env.get('CANVAS_API_TOKEN');
        this.canvasDomain = Env.get('CANVAS_API_DOMAIN');
    }

    async getCourse(courseId, options = {}) {
        //ISSUE HERE -- should not be called
        console.log('in real getCourse');
        const urlPrefix = `/courses/${courseId}`;
        let queryParams = [];
        if (options.includeTerm) {
            queryParams.push({ 'key': 'include[]', 'value': 'term' });
        }

        const results = await this.canvasGetRequest(urlPrefix, queryParams);
        return results;
    }

    async canvasGetRequest(urlPrefix, queryParams = []) {
        let url = `${this.canvasDomain}${urlPrefix}?per_page=100`;
        for (let queryParam of queryParams) {
            url += `&${encodeURIComponent(queryParam.key)}=${encodeURIComponent(queryParam.value)}`;
        }
        const headers = this.getAuthorizationHeader();
        let response = null;
        let returnedData = [];
        let requestsRemaining = true;
        let nextPaginationLink = false;
        let requestsMade = 0;

        while (requestsRemaining) {
            try {
                response = await axios.get(url, { headers: headers });

                //if an array, we may need to combine paginated results;
                //otherwise, just return the single or empty resource as is
                if (Array.isArray(response.data)) {
                    returnedData = returnedData.concat(response.data);
                }
                else {
                    returnedData = response.data;
                }

                requestsMade++;
                nextPaginationLink = this.getNextPaginationLink(response);

                if (!nextPaginationLink) {
                    requestsRemaining = false;
                }
                else {
                    url = nextPaginationLink.url;
                }

                //fail-safe here so we don't accidentally end up in an infinite loop or
                //land upon some Canvas resource that is too huge for us to handle
                if (requestsMade > 50) {
                    throw new Error('Exiting Canvas API, too many requests made to url: ' + url);
                }
            }
            catch (error) {
                requestsRemaining = false;
                throw new Error(this.getErrorMessage(error));
            }
        }

        if (returnedData.length < 2) {}

        return returnedData;
    }

    getNextPaginationLink(response) {
        const paginationLink = response.headers.link;
        let nextPaginationLink = false;

        if (paginationLink) {
            const parsedPaginationLink = parseLinkHeader(paginationLink);
            nextPaginationLink = parsedPaginationLink.next;
        }

        return nextPaginationLink;
    }

    getAuthorizationHeader() {
        return { "Authorization": `Bearer ${this.apiToken}` };
    }

    getErrorMessage(error) {
        let message = '';

        for (let errorMessage of error.response.data.errors) {
            message += (errorMessage.message + ' ');
        }

        return message;
    }
}

module.exports = CanvasApi;

Can't fake a module more than once during tests & issues faking module that other modules depend on

Hello, we are having some issues with faking a module in multiple tests. The faked version of the module is being carried over between tests. When a module depends on the 'faked' module, it will only use the first faked version that appears in all of the tests. More below.

Package version

4.0.9

Node.js and npm version

Node v12.5.0 & npm v6.9.0

Sample Code (to reproduce the issue)

A.JS
console.log('getting b')
const b = use('b')
module.exports = function() {
return b()
}


B.JS
module.exports = function() {
return 'real'
}


TEST.SPEC.JS
const { test, trait } = use('Test/Suite')('Post')
// ioc(inversion of control) is used to 'fake' methods for testing purposes
const { ioc } = use('@adonisjs/fold')
trait('Test/ApiClient')

test('If we can reproduce the bug', () => {
ioc.fake('b', () => {
return function() {
return 'fake'
}
})
const a = use('a')
console.log(a())
ioc.restore('b')
ioc.fake('b', () => {
return function() {
return 'super fake'
}
})
console.log(a())
ioc.restore('b')
})


OUTPUT AFTER RUNNING ADONIS TEST:
getting b
fake
fake
✓ If we can reproduce the bug (38ms)


DESCRIPTION:

We are having issues when it comes to faking modules for testing. When we fake a module that other modules depend on ('b' in the example), then once we call 'use' on the module that depends on it ('a' in the example), the module will load the 'faked' version. However, after restoring, and in a totally different test, the module ended up still using the 'faked' version of the module it depends on!('a' still had a faked 'b' even with a seperate 'use' call).

We noticed it is because nodejs's require.cache still recalls the previous 'required' version of a(the one which had the faked module b) and does not attempt to load it again to see the changes. As you can see in the example above, it only printed 'getting b' once even though there were more than 1 calls of "use('a')".

We can solve this by invalidating the cache(emptying it of the instance of a) but this is very messy as our actual code is much more complex and has many classes being loaded up. We'd have to know beforehand which classes were loaded involving the module we want to fake and remove them from the cache between tests.

Is there a solution using fold for this issue? What would you recommend we do to solve this issue?

Thanks,
Invigo

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.