@poppinss/manager
Advanced tools
Comparing version 1.1.4 to 2.0.0
@@ -1,29 +0,24 @@ | ||
export interface ManagerContract<DriverContract extends any, DriversList extends { | ||
export interface ManagerContract<DriverContract extends any, MappingsList extends { | ||
[key: string]: DriverContract; | ||
} = { | ||
[key: string]: DriverContract; | ||
}, DefaultDriver extends DriverContract = DriverContract> { | ||
driver<K extends keyof DriversList>(name: K): DriversList[K]; | ||
driver(name: string): DriverContract; | ||
driver(): DefaultDriver; | ||
}, DefaultItem extends DriverContract = DriverContract> { | ||
use<K extends keyof MappingsList>(name: K): MappingsList[K]; | ||
use(name: string): DefaultItem; | ||
use(): DefaultItem; | ||
extend(name: string, callback: (container: any) => DriverContract): void; | ||
} | ||
export declare type DriverNode<Implementation, Config> = { | ||
config: Config; | ||
implementation: Implementation; | ||
}; | ||
export declare type DriverNodesList<Implementation, Config> = { | ||
[key: string]: DriverNode<Implementation, Config>; | ||
}; | ||
export declare type ExtractDriversImpl<List extends DriverNodesList<any, any>> = { | ||
export declare type ExtractImplementations<List extends { | ||
[key: string]: { | ||
implementation: any; | ||
}; | ||
}> = { | ||
[P in keyof List]: List[P]['implementation']; | ||
}; | ||
export declare type ExtractDefaultDriverImpl<List extends DriverNodesList<any, any>, Config extends { | ||
driver: keyof List; | ||
}> = List[Config['driver']]['implementation']; | ||
export declare type ExtractDriversConfig<List extends DriverNodesList<any, any>> = { | ||
[P in keyof List]?: List[P]['config']; | ||
export declare type ExtractConfig<List extends { | ||
[key: string]: { | ||
config: any; | ||
}; | ||
}> = { | ||
[P in keyof List]: List[P]['config']; | ||
}; | ||
export declare type ExtractDefaultDriverConfig<List extends DriverNodesList<any, any>, Config extends { | ||
driver: keyof List; | ||
}> = List[Config['driver']]['config']; |
import { ManagerContract } from './contracts'; | ||
export declare abstract class Manager<DriverContract extends any, DriversList extends { | ||
export declare abstract class Manager<DriverContract extends any, MappingsList extends { | ||
[key: string]: DriverContract; | ||
} = { | ||
[key: string]: DriverContract; | ||
}, DefaultDriver extends DriverContract = DriverContract> implements ManagerContract<DriverContract, DriversList, DefaultDriver> { | ||
}, DefaultItem extends DriverContract = DriverContract> implements ManagerContract<DriverContract, MappingsList, DefaultItem> { | ||
protected $container: any; | ||
private _driversCache; | ||
private _mappingsCache; | ||
private _extendedDrivers; | ||
protected abstract $cacheDrivers: boolean; | ||
protected abstract getDefaultDriverName(): string; | ||
protected abstract $cacheMappings: boolean; | ||
protected abstract getDefaultMappingName(): string; | ||
protected abstract getMappingConfig(mappingName: string): any | undefined; | ||
protected abstract getMappingDriver(mappingName: string): string | undefined; | ||
constructor($container: any); | ||
private _getCachedDriver; | ||
private _saveDriverToCache; | ||
private _getFromCache; | ||
private _saveToCache; | ||
private _makeExtendedDriver; | ||
private _makeDriver; | ||
driver<K extends keyof DriversList>(name: K): DriversList[K]; | ||
driver(name: string): DriverContract; | ||
driver(): DefaultDriver; | ||
extend(name: string, callback: (container: any) => DriverContract): void; | ||
use<K extends keyof MappingsList>(name: K): MappingsList[K]; | ||
use(name: string): DriverContract; | ||
use(): DefaultItem; | ||
extend(name: string, callback: (container: any, mappingName: string, config: any) => DriverContract): void; | ||
} |
@@ -6,40 +6,41 @@ "use strict"; | ||
this.$container = $container; | ||
this._driversCache = {}; | ||
this._mappingsCache = new Map(); | ||
this._extendedDrivers = {}; | ||
} | ||
_getCachedDriver(name) { | ||
if (this.$cacheDrivers && this._driversCache[name]) { | ||
return this._driversCache[name]; | ||
} | ||
return null; | ||
_getFromCache(name) { | ||
return this._mappingsCache.get(name) || null; | ||
} | ||
_saveDriverToCache(name, value) { | ||
if (this.$cacheDrivers) { | ||
this._driversCache[name] = value; | ||
_saveToCache(name, value) { | ||
if (this.$cacheMappings) { | ||
this._mappingsCache.set(name, value); | ||
} | ||
} | ||
_makeExtendedDriver(name) { | ||
const value = this._extendedDrivers[name](this.$container); | ||
this._saveDriverToCache(name, value); | ||
_makeExtendedDriver(mappingName, driver, config) { | ||
const value = this._extendedDrivers[driver](this.$container, mappingName, config); | ||
this._saveToCache(mappingName, value); | ||
return value; | ||
} | ||
_makeDriver(name) { | ||
const driverCreatorName = `create${name.replace(/^\w|-\w/g, (g) => g.replace(/^-/, '').toUpperCase())}`; | ||
_makeDriver(mappingName, driver, config) { | ||
const driverCreatorName = `create${driver.replace(/^\w|-\w/g, (g) => g.replace(/^-/, '').toUpperCase())}`; | ||
if (typeof (this[driverCreatorName]) !== 'function') { | ||
throw new Error(`${name} driver is not supported by ${this.constructor.name}`); | ||
throw new Error(`${mappingName} driver is not supported by ${this.constructor.name}`); | ||
} | ||
const value = this[driverCreatorName](); | ||
this._saveDriverToCache(name, value); | ||
const value = this[driverCreatorName](mappingName, config); | ||
this._saveToCache(mappingName, value); | ||
return value; | ||
} | ||
driver(name) { | ||
name = (name || this.getDefaultDriverName()); | ||
const cached = this._getCachedDriver(name); | ||
use(name) { | ||
name = (name || this.getDefaultMappingName()); | ||
const cached = this._getFromCache(name); | ||
if (cached) { | ||
return cached; | ||
} | ||
if (this._extendedDrivers[name]) { | ||
return this._makeExtendedDriver(name); | ||
const driver = this.getMappingDriver(name); | ||
if (!driver) { | ||
throw new Error(`Make sure to define driver for ${name} mapping`); | ||
} | ||
return this._makeDriver(name); | ||
if (this._extendedDrivers[driver]) { | ||
return this._makeExtendedDriver(name, driver, this.getMappingConfig(name)); | ||
} | ||
return this._makeDriver(name, driver, this.getMappingConfig(name)); | ||
} | ||
@@ -46,0 +47,0 @@ extend(name, callback) { |
{ | ||
"name": "@poppinss/manager", | ||
"version": "1.1.4", | ||
"version": "2.0.0", | ||
"description": "The builder (Manager) pattern implementation", | ||
@@ -5,0 +5,0 @@ "scripts": { |
286
README.md
@@ -6,6 +6,19 @@ <div align="center"> | ||
# Manager/Builder pattern | ||
This module exposes a class making it easier to implement builder pattern to your classes. | ||
> Abstract class to incapsulate the behavior of constructing and re-using named objects. | ||
[![circleci-image]][circleci-url] [![npm-image]][npm-url] ![][typescript-image] [![license-image]][license-url] | ||
The module is used heavily by AdonisJs to build it's driver based features for components like `hash`, `mail`, `auth`, `social auth` and so on. | ||
If you are a user of AdonisJs, then you will be familiar with the following API. | ||
```ts | ||
mail.use('smtp').send() | ||
// or | ||
hash.use('argon2').hash('') | ||
``` | ||
The `use` method here constructs the driver for the mapping defined in the config file with the ability to cache the constructed drivers and add new drivers using the `extend` API. | ||
<!-- START doctoc generated TOC please keep comment here to allow auto update --> | ||
@@ -15,11 +28,11 @@ <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE --> | ||
- [Note](#note) | ||
- [Usage](#usage) | ||
- [How it works?](#how-it-works) | ||
- [Drivers](#drivers) | ||
- [Session main class](#session-main-class) | ||
- [Usage](#usage-1) | ||
- [Using Manager class](#using-manager-class) | ||
- [Adding drivers from outside](#adding-drivers-from-outside) | ||
- [Autocomplete drivers list](#autocomplete-drivers-list) | ||
- [Drivers types](#drivers-types) | ||
- [Extending drivers](#extending-drivers) | ||
- [What is container?](#what-is-container) | ||
- [Typescript types](#typescript-types) | ||
- [Driver interface](#driver-interface) | ||
- [`use` method intellisense](#use-method-intellisense) | ||
- [Typed config](#typed-config) | ||
- [Updating mapping list generic for Manager](#updating-mapping-list-generic-for-manager) | ||
- [API Docs](#api-docs) | ||
@@ -30,5 +43,9 @@ - [Maintainers](#maintainers) | ||
## Note | ||
The API for `2.x.x` is completely different from `1.x.x`, since the newer version supports multiple mappings of a single driver. [Click here](https://github.com/poppinss/manager/tree/v1.1.4) to check docs for older version. | ||
## Usage | ||
Install the package from npm as follows: | ||
Install the package from npm registry as follows: | ||
```sh | ||
@@ -41,118 +58,233 @@ npm i @poppinss/manager | ||
and then import it as follows. Check [How it works](#how-it-works) section for complete docs | ||
Let's imagine we are building a mailer (with dummy implementation) that supports multiple drivers and each driver can be used for multiple times. Example config for same | ||
```ts | ||
import { Manager, ManagerContract } from '@poppinss/manager' | ||
```js | ||
{ | ||
mailer: 'transactional', | ||
mailers: { | ||
transactional: { | ||
👇 // driver is important | ||
driver: 'smtp', | ||
host: '', | ||
user: '', | ||
password: '' | ||
}, | ||
promotional: { | ||
driver: 'mailchimp', | ||
apiKey: '' | ||
} | ||
} | ||
} | ||
``` | ||
## How it works? | ||
[Builder pattern](https://dzone.com/articles/design-patterns-the-builder-pattern) is a way to compose objects by hiding most of their complexities. Similarly, the Manager pattern also manages the lifecycle of those objects, after composing them. | ||
Our module supports two drivers `smtp` and `mailchimp` and here's how we can construct them. | ||
AdonisJs heavily makes use of this pattern in modules like `Mail`, `Auth`, `Session` and so on, where you can switch between multiple drivers, without realizing the amount of work done behind the scenes. | ||
Let's see how the driver's based approach works without the Manager pattern. | ||
#### Drivers | ||
```ts | ||
class SessionRedisDriver { | ||
public write () {} | ||
public read () {} | ||
class SmtpDriver { | ||
constructor (config) {} | ||
send () {} | ||
} | ||
``` | ||
class SessionFileDriver { | ||
public write () {} | ||
public read () {} | ||
```ts | ||
class MailchimpDriver { | ||
constructor (config) {} | ||
send () {} | ||
} | ||
``` | ||
#### Session main class | ||
The consumer of our code can import and use these drivers manually by constructing a new instance every time. However, we can improve the developer experience, by creating the following manager class. | ||
```ts | ||
class Session { | ||
constructor (private _driver) { | ||
import { Manager } from '@poppinss/manager' | ||
class Mailer extends Manager { | ||
constructor (container, private _config) { | ||
super(container) | ||
} | ||
protected $cacheMappings = true | ||
protected getDefaultMappingName (): string { | ||
return this._config.mailer | ||
} | ||
put () { | ||
this._driver.put() | ||
protected getMappingConfig (name): any { | ||
return this._config.mailers[name] | ||
} | ||
protected getMappingDriver (name): any { | ||
return this._config.mailers[name].driver | ||
} | ||
protected createSmtp (name, config) { | ||
return new Smtp(config) | ||
} | ||
protected createMailchimp (name, config) { | ||
return new MailchimpDriver(config) | ||
} | ||
} | ||
``` | ||
#### Usage | ||
Now, you can use it as follows. | ||
The `Manager` class forces the parent class to define some of the required methods/properties. | ||
- **$cacheMappings**: When set to true, the manager will internally cache the instance of drivers and will re-use them. | ||
- **getDefaultMappingName**: The name of the default mapping. In this case, it is the name of the `mailer` set in the config | ||
- **getMappingDriver**: Returning the config for a mapping. We pull it from the `mailers` object defined in the config. | ||
- **getMappingDriver**: Returning the driver for a mapping inside config. | ||
The other two methods `createSmtp` and `createMailchimp` are the methods invoked when the end user will access a driver. This is how it works. | ||
```ts | ||
const file = new SessionFileDriver() | ||
const session = new Session(file) | ||
session.put('') | ||
const mailer = new Mailer({}, config) | ||
mailer.use('smtp').send() | ||
``` | ||
The above example is very simple and may feel fine at first glance. However, you may face following challenges. | ||
The `mailer.use('transactional')` will invoke `createSmtp` as part of the following convention. | ||
1. It is not easy to switch drivers, since you have to manually re-create the driver and then create instance of the session class. | ||
2. If you want to use singleton instances, then you need to wrap all this code inside a seperate file, that exports the singleton. | ||
3. If creating a driver has more dependencies, then you will have to construct all those dependencies by hand. | ||
- `create` + `PascalCaseDriverName` | ||
- `create` + `Smtp` = `createSmtp` | ||
- `create` + `Mailchimp` = `createMailchimp` | ||
## Using Manager class | ||
Continuing to the same example, let's create a Manager class that abstracts away all of this manual boilerplate. | ||
> **NOTE**: You need one method for each driver and not the mapping. The mapping names can be anything the user wants to keep in their config file. | ||
## Extending drivers | ||
The manager class also exposes the API to add new drivers from outside in and this is done using the `extend` method. | ||
```ts | ||
import { Manager } from '@poppinss/manager' | ||
mailer.extend('postmark', (container, name, config) => { | ||
return new Postmark(config) | ||
}) | ||
``` | ||
class SessionManager extends Manager<Session> { | ||
protected getDefaultDriverName () { | ||
return 'file' | ||
} | ||
## What is container? | ||
// Create singleton instances and re-use them | ||
protected $cacheDrivers = true | ||
The `Manager` class needs a container value, which is then passed to the `extended` drivers. This can be anything from application state to an empty object. | ||
protected createFile () { | ||
return new Session(new SessionFileDriver()) | ||
} | ||
In case of AdonisJs, it is the instance of the IoC container, so that the outside world (extended drivers) can pull dependencies from the container. | ||
protected createRedis () { | ||
return new Session(new SessionRedisDriver()) | ||
} | ||
## Typescript types | ||
In order for intellisense to work, you have do some ground work of defining additional types. This in-fact is a common theme with static languages, that you have to rely on loose coupling when creating or using extensible objects. | ||
### Driver interface | ||
The first thing you must have in place is the interface that every driver adheres to. | ||
```ts | ||
interface MailDriverContract { | ||
send (): void | ||
} | ||
``` | ||
and now, you can use it as follows. | ||
And then pass it as a generic to the `Manager` constructor | ||
```ts | ||
const session = new SessionManager() | ||
session.driver('file') // redis instance | ||
session.driver('redis') // file instance | ||
class Mailer extends Manager<MailDriverContract> { | ||
// rest of the code | ||
} | ||
``` | ||
## Adding drivers from outside | ||
Also, the session manager can be extended to add more dynamic drivers. Think of accepting plugins to add drivers. | ||
Now, `extend` method will ensure that all drivers adhere to the `MailDriverContract`. | ||
### `use` method intellisense | ||
Currently the output of `use` method will be typed to `MailDriverContract` and not the actual implementation class. This is fine, **when all drivers have the same properties, methods and output**. However, you can define a list of mappings as follows | ||
```ts | ||
const session = new SessionManager() | ||
session.extend('mongo', () => { | ||
return new SessionMongoDriver() | ||
}) | ||
session.driver('mongo') // MongoDriver | ||
type MappingsList = { | ||
transactional: SmtpDriver, | ||
promotional: MailchimpDriver, | ||
} | ||
``` | ||
## Autocomplete drivers list | ||
Also, you can pass a union to the `Manager` constructor and it will typehint the list of drivers for you. | ||
And then pass this mapping as a 2nd argument to the `Manager` constructor. | ||
```ts | ||
class SessionManager extends Manager< | ||
Session, | ||
{ file: SessionFileDriver, redis: SessionRedisDriver }, | ||
class Mailer extends Manager< | ||
MailDriverContract, | ||
MappingsList | ||
> { | ||
} | ||
``` | ||
new SessionManager | ||
.driver('') // typehints 'file' and 'redis' | ||
The `use` method will now return the concrete type. You can also use this same mapping to enforce correct configuration. | ||
```ts | ||
type SmtpConfig = { | ||
driver: 'smtp', | ||
host: string, | ||
user: string, | ||
password: string, | ||
port?: number, | ||
} | ||
type MailchimpConfig = { | ||
driver: 'mailchimp', | ||
apiKey: string, | ||
} | ||
type MappingsList = { | ||
transactional: { | ||
config: SmtpConfig, | ||
implementation: SmtpDriver, | ||
}, | ||
promotional: { | ||
config: MailchimpConfig, | ||
implementation: MailchimpDriver, | ||
}, | ||
} | ||
``` | ||
## Drivers types | ||
Along with the runtime code to extend and fetch drivers, we also ship the types to deal with static side of things as well. | ||
The master `MappingsList` will ensure that static types and the configuration is always referring to the same driver instance. | ||
Since the process of having extensible drivers with type information requires some complex types structure, we recommend you to look at [example/index.ts](example/index.ts) file for a complete example. | ||
#### Typed config | ||
```ts | ||
import { ExtractConfig } from '@poppinss/manager' | ||
type Config<Mailer extends keyof MappingsList> = { | ||
mailer: Mailer, | ||
mailers: ExtractConfig<MappingsList>, | ||
} | ||
const config: Config<'transactional'> = { | ||
mailer: 'transactional', | ||
mailers: { | ||
transactional: { | ||
driver: 'smtp', | ||
host: '', | ||
user: '', | ||
password: '', | ||
}, | ||
promotional: { | ||
driver: 'mailchimp', | ||
apiKey: '', | ||
}, | ||
}, | ||
} | ||
``` | ||
#### Updating mapping list generic for Manager | ||
```ts | ||
import { ExtractImplementations, Manager } from '@poppinss/manager' | ||
class Mailer extends Manager< | ||
MailDriverContract, | ||
ExtractImplementations<MappingsList> | ||
> { | ||
} | ||
``` | ||
Finally, you have runtime functionality to switch between multiple mailers and also have type safety for same. | ||
## API Docs | ||
@@ -159,0 +291,0 @@ Following are the autogenerated files via Typedoc |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
15653
305
0
105