Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@poppinss/manager

Package Overview
Dependencies
Maintainers
1
Versions
33
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@poppinss/manager - npm Package Compare versions

Comparing version 1.1.4 to 2.0.0

37

build/src/contracts.d.ts

@@ -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": {

@@ -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

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc