diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index cdbda08..d64d0fb 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -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) @@ -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) @@ -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) @@ -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) @@ -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) @@ -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) diff --git a/src/index.ts b/src/index.ts index c6de224..7f202ce 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,6 +19,7 @@ import { mergeInject, autoInject, alsoInject, + unInject, type, extra, singleton, @@ -973,7 +974,7 @@ class Knifecycle { Autoloader>> | 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( @@ -1293,6 +1294,7 @@ export { mergeInject, autoInject, alsoInject, + unInject, extra, singleton, reuseSpecialProps, diff --git a/src/util.test.ts b/src/util.test.ts index e6f3eba..79d3054 100644 --- a/src/util.test.ts +++ b/src/util.test.ts @@ -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'; @@ -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 = diff --git a/src/util.ts b/src/util.ts index 20b1653..1be7d78 100644 --- a/src/util.ts +++ b/src/util.ts @@ -621,6 +621,68 @@ export function inject, S>( return uniqueInitializer; } +/** + * Decorator creating a new initializer omitting + * the given dependencies. + * @param {Array} 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, S>( + dependencies: DependencyDeclaration[], + initializer: ProviderInitializer, +): ProviderInitializer; +export function unInject, S>( + dependencies: DependencyDeclaration[], + initializer: ProviderInitializerBuilder, +): ProviderInitializerBuilder; +export function unInject, S>( + dependencies: DependencyDeclaration[], + initializer: ServiceInitializer, +): ServiceInitializer; +export function unInject, S>( + dependencies: DependencyDeclaration[], + initializer: ServiceInitializerBuilder, +): ServiceInitializerBuilder; +export function unInject, S>( + dependencies: DependencyDeclaration[], + initializer: + | ProviderInitializerBuilder + | ServiceInitializerBuilder, +): ProviderInitializerBuilder | ServiceInitializerBuilder { + const filteredDependencies = dependencies.map(parseDependencyDeclaration); + const originalDependencies = (initializer[SPECIAL_PROPS.INJECT] || []).map( + parseDependencyDeclaration, + ); + + return inject( + originalDependencies.filter(({ serviceName }) => + filteredDependencies.every( + ({ serviceName: filteredServiceName }) => + serviceName !== filteredServiceName, + ), + ).map(stringifyDependencyDeclaration), + initializer as ServiceInitializerBuilder, + ); +} + /** * Apply injected dependencies from the given initializer to another one * @param {Function} from The initialization function in which to pick the dependencies @@ -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.