Skip to content

Commit

Permalink
feat(utils): add a way to uninject a dependency
Browse files Browse the repository at this point in the history
  • Loading branch information
nfroidure committed Nov 7, 2024
1 parent 796f9e0 commit 7f90e65
Show file tree
Hide file tree
Showing 4 changed files with 130 additions and 9 deletions.
14 changes: 7 additions & 7 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ It is designed to have a low footprint on services code.
In fact, the Knifecycle API is aimed to allow to statically
build its services load/unload code once in production.

[See in context](./src/index.ts#L201-L220)
[See in context](./src/index.ts#L202-L221)



Expand All @@ -52,7 +52,7 @@ A service provider is full of state since its concern is
[encapsulate](https://en.wikipedia.org/wiki/Encapsulation_(computer_programming))
your application global states.

[See in context](./src/index.ts#L222-L231)
[See in context](./src/index.ts#L223-L232)



Expand Down Expand Up @@ -92,7 +92,7 @@ The `?` flag indicates an optional dependency.
It allows to write generic services with fixed
dependencies and remap their name at injection time.

[See in context](./src/util.ts#L1304-L1313)
[See in context](./src/util.ts#L1366-L1375)



Expand Down Expand Up @@ -121,7 +121,7 @@ Initializers can be of three types:
instanciated once for all for each executions silos using
them (we will cover this topic later on).

[See in context](./src/index.ts#L311-L335)
[See in context](./src/index.ts#L312-L336)



Expand All @@ -137,7 +137,7 @@ Depending on your application design, you could run it
in only one execution silo or into several ones
according to the isolation level your wish to reach.

[See in context](./src/index.ts#L644-L654)
[See in context](./src/index.ts#L645-L655)



Expand Down Expand Up @@ -166,12 +166,12 @@ For the build to work, we need:
Sadly TypeScript does not allow to add generic types
in all cases. This is why `(Service|Provider)Initializer`
types do not embed the `(Service|Provider)Properties`
direclty. Instead, we use this utility function to
directly. Instead, we use this utility function to
reveal it to TypeScript and, by the way, check their
completeness at execution time.

For more details, see:
https://stackoverflow.com/questions/64948037/generics-type-loss-while-infering/64950184#64950184

[See in context](./src/util.ts#L1374-L1385)
[See in context](./src/util.ts#L1436-L1447)

4 changes: 3 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
mergeInject,
autoInject,
alsoInject,
unInject,
type,
extra,
singleton,
Expand Down Expand Up @@ -973,7 +974,7 @@ class Knifecycle {
Autoloader<Initializer<unknown, Dependencies<unknown>>> | undefined
> {
// The auto loader must only have static dependencies
// and we have to do this check here to avoid inifinite loop
// and we have to do this check here to avoid infinite loop
if (parentsNames.includes(AUTOLOAD)) {
debug(
`${parentsNames.join(
Expand Down Expand Up @@ -1293,6 +1294,7 @@ export {
mergeInject,
autoInject,
alsoInject,
unInject,
extra,
singleton,
reuseSpecialProps,
Expand Down
57 changes: 57 additions & 0 deletions src/util.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
handler,
autoHandler,
SPECIAL_PROPS,
unInject,
} from './util.js';
import type { Provider } from './util.js';
import type { Dependencies, ServiceInitializer } from './index.js';
Expand Down Expand Up @@ -258,6 +259,62 @@ describe('mergeInject', () => {
});
});

describe('unInject', () => {
test('should work with empty dependencies', () => {
const baseProvider =
async ({ ENV, mysql: db }) =>
async () => ({
ENV,
db,
});
const removedDependencies = [];
const leftDependencies = [];
const dependencies = [...removedDependencies, ...leftDependencies];
const initializer = inject(dependencies, baseProvider);
const newInitializer = unInject(removedDependencies, initializer);

assert.notEqual(newInitializer, baseProvider);
assert.notEqual(newInitializer[SPECIAL_PROPS.INJECT], dependencies);
assert.deepEqual(newInitializer[SPECIAL_PROPS.INJECT], leftDependencies);
});

test('should allow to remove dependencies', () => {
const baseProvider =
async ({ ENV, mysql: db }) =>
async () => ({
ENV,
db,
});
const removedDependencies = ['mysql'];
const leftDependencies = ['ENV'];
const dependencies = [...removedDependencies, ...leftDependencies];
const initializer = inject(dependencies, baseProvider);
const newInitializer = unInject(removedDependencies, initializer);

assert.notEqual(newInitializer, baseProvider);
assert.notEqual(newInitializer[SPECIAL_PROPS.INJECT], dependencies);
assert.deepEqual(newInitializer[SPECIAL_PROPS.INJECT], leftDependencies);
});

test('should allow to remove mapped dependencies', () => {
const baseProvider =
async ({ ENV, mysql: db }) =>
async () => ({
ENV,
db,
});
const removedDependencies = ['mysql>myMysql'];
const leftDependencies = ['ENV>myENV'];
const dependencies = ['mysql>anotherMysql', ...leftDependencies];
const initializer = inject(dependencies, baseProvider);
const newInitializer = unInject(removedDependencies, initializer);

assert.notEqual(newInitializer, baseProvider);
assert.notEqual(newInitializer[SPECIAL_PROPS.INJECT], dependencies);
assert.deepEqual(newInitializer[SPECIAL_PROPS.INJECT], leftDependencies);
});
});

describe('autoInject', () => {
test('should allow to decorate an initializer with dependencies', () => {
const baseProvider =
Expand Down
64 changes: 63 additions & 1 deletion src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -621,6 +621,68 @@ export function inject<D extends Dependencies<any>, S>(
return uniqueInitializer;
}

/**
* Decorator creating a new initializer omitting
* the given dependencies.
* @param {Array<String>} dependencies
* List of dependencies to omit (also accept dependencies
* declarations but omit the destination part)
* @param {Function} initializer
* The initializer to tweak
* @return {Function}
* Returns a new initializer
* @example
*
* import Knifecycle, { unInject } from 'knifecycle'
* import myServiceInitializer from './service';
*
* new Knifecycle()
* .register(
* service(
* unInject(['ENV'], myServiceInitializer)
* 'myService',
* )
* )
* );
*/
export function unInject<D extends Dependencies<any>, S>(
dependencies: DependencyDeclaration[],
initializer: ProviderInitializer<any, S>,
): ProviderInitializer<D, S>;
export function unInject<D extends Dependencies<any>, S>(
dependencies: DependencyDeclaration[],
initializer: ProviderInitializerBuilder<any, S>,
): ProviderInitializerBuilder<D, S>;
export function unInject<D extends Dependencies<any>, S>(
dependencies: DependencyDeclaration[],
initializer: ServiceInitializer<any, S>,
): ServiceInitializer<D, S>;
export function unInject<D extends Dependencies<any>, S>(
dependencies: DependencyDeclaration[],
initializer: ServiceInitializerBuilder<any, S>,
): ServiceInitializerBuilder<D, S>;
export function unInject<D extends Dependencies<any>, S>(
dependencies: DependencyDeclaration[],
initializer:
| ProviderInitializerBuilder<any, S>
| ServiceInitializerBuilder<any, S>,
): ProviderInitializerBuilder<D, S> | ServiceInitializerBuilder<D, S> {
const filteredDependencies = dependencies.map(parseDependencyDeclaration);
const originalDependencies = (initializer[SPECIAL_PROPS.INJECT] || []).map(
parseDependencyDeclaration,
);

return inject<D, S>(
originalDependencies.filter(({ serviceName }) =>
filteredDependencies.every(
({ serviceName: filteredServiceName }) =>
serviceName !== filteredServiceName,
),
).map(stringifyDependencyDeclaration),
initializer as ServiceInitializerBuilder<D, S>,
);
}

/**
* Apply injected dependencies from the given initializer to another one
* @param {Function} from The initialization function in which to pick the dependencies
Expand Down Expand Up @@ -1376,7 +1438,7 @@ export function stringifyDependencyDeclaration(
Sadly TypeScript does not allow to add generic types
in all cases. This is why `(Service|Provider)Initializer`
types do not embed the `(Service|Provider)Properties`
direclty. Instead, we use this utility function to
directly. Instead, we use this utility function to
reveal it to TypeScript and, by the way, check their
completeness at execution time.
Expand Down

0 comments on commit 7f90e65

Please sign in to comment.