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

Add option to detect and error on dependency lifetime mismatch #349

Merged
merged 17 commits into from
Jan 7, 2024
Merged
Show file tree
Hide file tree
Changes from 11 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
246 changes: 147 additions & 99 deletions README.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion examples/babel/.babelrc
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"presets": ["env"]
"presets": ["es2015"]
}
1 change: 1 addition & 0 deletions examples/babel/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { DependentService } from './services/dependentService'

const container = createContainer({
injectionMode: InjectionMode.CLASSIC,
strict: true,
})

container.register({
Expand Down
10 changes: 6 additions & 4 deletions examples/koa/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,16 @@ const app = new Koa()
const router = new KoaRouter()

// Create a container.
const container = createContainer()
const container = createContainer({ strict: true })

// Register usefull stuff
// Register useful stuff
const MessageService = require('./services/MessageService')
const makeMessageRepository = require('./repositories/messageRepository')
container.register({
// used by the repository.
DB_CONNECTION_STRING: asValue('localhost:1234'),
// used by the repository; registered.
DB_CONNECTION_STRING: asValue('localhost:1234', {
lifetime: awilix.Lifetime.SINGLETON,
}),
// resolved for each request.
messageService: asClass(MessageService).scoped(),
// only resolved once
Expand Down
4 changes: 3 additions & 1 deletion examples/simple/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
const awilix = require('../..')

// Create a container.
const container = awilix.createContainer()
const container = awilix.createContainer({
strict: true,
})

// Register some value.. We depend on this in `Stuffs.js`
container.register('database', awilix.asValue('stuffs_db'))
Expand Down
3 changes: 2 additions & 1 deletion examples/typescript/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// This is largely for testing, but import what we need
import { createContainer, asClass, InjectionMode } from '../../../src/awilix'
import { createContainer, asClass, InjectionMode } from '../../..'
import TestService from './services/TestService'
import DependentService from './services/DependentService'

Expand All @@ -11,6 +11,7 @@ interface ICradle {
// Create the container
const container = createContainer<ICradle>({
injectionMode: InjectionMode.CLASSIC,
strict: true,
})

// Register the classes
Expand Down
5 changes: 3 additions & 2 deletions examples/typescript/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
"outDir": "dist",
"sourceMap": false,
"target": "es5",
"noImplicitAny": true
"noImplicitAny": true,
"rootDir": "./src",
},
"include": ["src/**/*.ts"],
"include": ["**/*.ts"],
"exclude": ["dist", "node_modules"]
}
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

170 changes: 169 additions & 1 deletion src/__tests__/container.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as util from 'util'
import { createContainer, AwilixContainer } from '../container'
import { Lifetime } from '../lifetime'
import { AwilixResolutionError } from '../errors'
import { asClass, asFunction, asValue } from '../resolvers'
import { aliasTo, asClass, asFunction, asValue } from '../resolvers'
import { InjectionMode } from '../injection-mode'

class Test {
Expand Down Expand Up @@ -298,6 +298,8 @@ describe('container', () => {
expect(scope2.cradle.counterValue === 2).toBe(true)

expect(scope1Child.cradle.counterValue === 3).toBe(true)
// assert that the parent scope was not affected
expect(scope1.cradle.counterValue === 1).toBe(true)
})

it('supports nested scopes', () => {
Expand Down Expand Up @@ -588,6 +590,172 @@ describe('container', () => {
expect(theAnswer()).toBe(42)
})
})

describe('automatic asValue lifetime handling', () => {
it('sets values to singleton lifetime when registered on the root container', () => {
const container = createContainer()
container.register({
val: asValue(42),
})
const registration = container.getRegistration('val')
expect(registration).toBeTruthy()
expect(registration!.lifetime).toBe(Lifetime.SINGLETON)
})
it('sets values to scoped lifetime when registered on a scope container', () => {
const container = createContainer()
const scope = container.createScope()
scope.register({
val: asValue(42),
})
const registration = scope.getRegistration('val')
expect(registration).toBeTruthy()
expect(registration!.lifetime).toBe(Lifetime.SCOPED)
})
})

describe('strict mode', () => {
describe('lifetime mismatch check', () => {
it('allows longer lifetime modules to depend on shorter lifetime dependencies by default', () => {
const container = createContainer()
container.register({
first: asFunction((cradle: any) => cradle.second, {
lifetime: Lifetime.SCOPED,
}),
second: asFunction(() => 'hah'),
})

expect(container.resolve('first')).toBe('hah')
})

it('throws an AwilixResolutionError when longer lifetime modules depend on shorter lifetime dependencies and strict is set', () => {
const container = createContainer({
strict: true,
})
container.register({
first: asFunction((cradle: any) => cradle.second, {
lifetime: Lifetime.SCOPED,
}),
second: asFunction(() => 'hah'),
})

const err = throws(() => container.resolve('first'))
expect(err.message).toContain('first -> second')
expect(err.message).toContain(
"Dependency 'second' has a shorter lifetime than its ancestor: 'first'",
)
})

it('does not throw an error when an injector proxy is used and strict is set', () => {
const container = createContainer({
strict: true,
})
container.register({
first: asFunction((cradle: any) => cradle.injected, {
lifetime: Lifetime.SCOPED,
}).inject(() => ({ injected: 'hah' })),
fnimick marked this conversation as resolved.
Show resolved Hide resolved
})

expect(container.resolve('first')).toBe('hah')
})

it('allows for asValue() to be used when strict is set', () => {
const container = createContainer({
strict: true,
})
container.register({
first: asFunction((cradle: any) => cradle.val, {
lifetime: Lifetime.SCOPED,
}),
second: asFunction((cradle: any) => cradle.secondVal, {
lifetime: Lifetime.SINGLETON,
}),
val: asValue('hah'),
secondVal: asValue('foobar'),
})

expect(container.resolve('first')).toBe('hah')
expect(container.resolve('second')).toBe('foobar')
})

it('correctly errors when a singleton parent depends on a scoped value and strict is set', () => {
const container = createContainer({
strict: true,
})
container.register({
first: asFunction((cradle: any) => cradle.second, {
lifetime: Lifetime.SINGLETON,
}),
second: asValue('hah'),
})
const scope = container.createScope()
scope.register({
second: asValue('foobar'),
})

const err = throws(() => scope.resolve('first'))
expect(err.message).toContain('first -> second')
expect(err.message).toContain(
"Dependency 'second' has a shorter lifetime than its ancestor: 'first'",
)
})

it('allows aliasTo to be used when strict is set', () => {
const container = createContainer({
strict: true,
})
container.register({
first: asFunction((cradle: any) => cradle.second, {
lifetime: Lifetime.SINGLETON,
}),
second: aliasTo('val'),
val: asValue('hah'),
})

expect(container.resolve('first')).toBe('hah')
})

it('detects when an aliasTo resolution violates lifetime constraints', () => {
const container = createContainer({
strict: true,
})
container.register({
first: asFunction((cradle: any) => cradle.second, {
lifetime: Lifetime.SINGLETON,
}),
second: aliasTo('val'),
val: asValue('hah'),
})
const scope = container.createScope()
scope.register({
val: asValue('foobar'),
})

const err = throws(() => scope.resolve('first'))
expect(err.message).toContain('first -> second -> val')
expect(err.message).toContain(
"Dependency 'val' has a shorter lifetime than its ancestor: 'first'",
)
})
})

describe('singleton registration on scope check', () => {
it('detects and errors when a singleton is registered on a scope', () => {
const container = createContainer({
strict: true,
})
const scope = container.createScope()
const err = throws(() =>
scope.register({
test: asFunction(() => 42, { lifetime: Lifetime.SINGLETON }),
}),
)
expect(err.message).toContain("Could not register 'test'")
expect(err.message).toContain(
'Cannot register a singleton on a scoped container',
)
})
})
})
})

describe('setting a name on the registration options', () => {
Expand Down
15 changes: 15 additions & 0 deletions src/__tests__/lifetime.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Lifetime, isLifetimeLonger } from '../lifetime'

describe('isLifetimeLonger', () => {
it('correctly compares lifetimes', () => {
fnimick marked this conversation as resolved.
Show resolved Hide resolved
expect(isLifetimeLonger(Lifetime.TRANSIENT, Lifetime.TRANSIENT)).toBe(false)
expect(isLifetimeLonger(Lifetime.TRANSIENT, Lifetime.SCOPED)).toBe(false)
expect(isLifetimeLonger(Lifetime.TRANSIENT, Lifetime.SINGLETON)).toBe(false)
expect(isLifetimeLonger(Lifetime.SCOPED, Lifetime.TRANSIENT)).toBe(true)
expect(isLifetimeLonger(Lifetime.SCOPED, Lifetime.SCOPED)).toBe(false)
expect(isLifetimeLonger(Lifetime.SCOPED, Lifetime.SINGLETON)).toBe(false)
expect(isLifetimeLonger(Lifetime.SINGLETON, Lifetime.TRANSIENT)).toBe(true)
expect(isLifetimeLonger(Lifetime.SINGLETON, Lifetime.SCOPED)).toBe(true)
expect(isLifetimeLonger(Lifetime.SINGLETON, Lifetime.SINGLETON)).toBe(false)
})
})
Loading