This commit is contained in:
Tutur33
2023-11-24 22:35:41 +01:00
parent 3c0b507a93
commit 7644b2a0f7
45165 changed files with 4803356 additions and 3 deletions
+9
View File
@@ -0,0 +1,9 @@
# The MIT License
Copyright 2021 Harminder Virk, contributors
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+283
View File
@@ -0,0 +1,283 @@
<div align="center">
<img src="https://res.cloudinary.com/adonisjs/image/upload/q_100/v1557762307/poppinss_iftxlt.jpg" width="600px">
</div>
# Manager Pattern
> Implementation of the Manager pattern used by AdonisJS
[![gh-workflow-image]][gh-workflow-url] [![typescript-image]][typescript-url] [![npm-image]][npm-url] [![license-image]][license-url] [![synk-image]][synk-url]
Manager pattern is a way to ease the construction of objects of similar nature. To understand it better, we will follow an imaginary example through out this document.
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
## Table of contents
- [Scanerio](#scanerio)
- [Basic implementation](#basic-implementation)
- [Using a Manager Class](#using-a-manager-class)
- [Step1: Define a sample config object.](#step1-define-a-sample-config-object)
- [Step 2: Create manager class and accept mappings config](#step-2-create-manager-class-and-accept-mappings-config)
- [Step 3: Using the config to constructor drivers](#step-3-using-the-config-to-constructor-drivers)
- [Step 4: Move drivers construction into the manager class](#step-4-move-drivers-construction-into-the-manager-class)
- [Usage](#usage)
- [Extending from outside-in](#extending-from-outside-in)
- [Driver Interface](#driver-interface)
- [Defining Drivers Interface](#defining-drivers-interface)
- [Passing interface to Manager](#passing-interface-to-manager)
- [Mappings Type](#mappings-type)
- [Mapping suggestions](#mapping-suggestions)
- [Return type](#return-type)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
## Scanerio
Let's imagine we are creating a mailing library and it supports multiple drivers like: **SMTP**, **Mailgun**, **PostMark** and so on. Also, we want the users of our library to use each driver for multiple times using different configuration. For example:
Using the **Mailgun driver with different accounts**. Maybe one account for sending promotional emails and another account for sending transactional emails.
## Basic implementation
The simplest way to expose the drivers, is to export them directly and let the consumer construct instances of them. For example:
```ts
import { Mailgun } from 'my-mailer-library'
const promotional = new Mailgun(configForPromtional)
const transactional = new Mailgun(configForTransactional)
promotional.send()
transactional.send()
```
The above approach works perfect, but has few drawbacks
- If the construction of the drivers needs more than one constructor arguments, then it will become cumbersome for the consumer to satisfy all those dependencies.
- They will have to manually manage the lifecycle of the constructed objects. Ie `promotional` and `transactional` in this case.
## Using a Manager Class
What we really need is a Manager to manage and construct these objects in the most ergonomic way.
### Step1: Define a sample config object.
First step is to move the mappings to a configuration file. The config mimics the same behavior we were trying to achieve earlier (in the Basic example), but defines it declaratively this time.
```ts
const mailersConfig = {
default: 'transactional',
list: {
transactional: {
driver: 'mailgun',
},
promotional: {
driver: 'mailgun',
},
},
}
```
### Step 2: Create manager class and accept mappings config
```ts
import { Manager } from '@poppinss/manager'
class MailManager implements Manager {
protected singleton = true
constructor(private config) {
super({})
}
}
```
### Step 3: Using the config to constructor drivers
The **Base Manager** class will do all the heavy lifting for you. However, you will have to define certain methods to resolve the values from the config file.
```ts
import { Manager } from '@poppinss/manager'
class MailManager implements Manager {
protected singleton = true
protected getDefaultMappingName() {
return this.config.default
}
protected getMappingConfig(mappingName: string) {
return this.config.list[mappingName]
}
protected getMappingDriver(mappingName: string) {
return this.config.list[mappingName].driver
}
constructor(private config) {
super({})
}
}
```
### Step 4: Move drivers construction into the manager class
The final step is to write the code for constructing drivers. The **Base Manager** uses a convention for this. Anytime the consumer will ask for the `mailgun` driver, it will invoke `createMailgun` method. So the convention here is to prefix `create` followed by the camelCase driver name.
```ts
import { Manager } from '@poppinss/manager'
class MailManager implements Manager {
// ... The existing code
public createMailgun(mappingName, config) {
return new Mailgun(config)
}
public createSmtp(mappingName, config) {
return new Smtp(config)
}
}
```
## Usage
Once done, the consumer of the Mailer class just needs to define the mappings config and they are good to go.
```ts
const mailersConfig = {
default: 'transactional',
list: {
transactional: {
driver: 'mailgun',
},
promotional: {
driver: 'mailgun',
},
},
}
const mailer = new MailManager(mailersConfig)
mailer.use('transactional').send()
mailer.use('promotional').send()
```
- The lifecycle of the mailers is now encapsulated within the manager class. The consumer can call `mailer.use()` as many times as they want, without worrying about creating too many un-used objects.
- They just need to define the mailers config once and get rid of any custom code required to construct individual drivers.
## Extending from outside-in
The Base Manager class comes with first class support for adding custom drivers from outside-in using the `extend` method.
```ts
const mailer = new MailManager(mailersConfig)
mailer.extend('postmark', (manager, mappingName, config) => {
return new PostMark(config)
})
```
The `extend` method receives a total of three arguments:
- The `manager` object is the reference to the `mailer` object.
- The name of the mapping inside the config file.
- The actual configuration object.
- The `callback` should return an instance of the Driver.
## Driver Interface
The Manager class can also leverage static Typescript types to have better intellisense support and also ensure that the drivers added using the `extend` method adhere to a given interface.
### Defining Drivers Interface
Following is a dummy interface, we expect all drivers to adhere too
```ts
interface DriverContract {
send(): Promise<void>
}
```
### Passing interface to Manager
```ts
import { Manager } from '@poppinss/manager'
import { DriverContract } from './Contracts'
class MailManager implements Manager<DriverContract> {}
```
## Mappings Type
The mappings config currently has `any` type and hence, the `mailer.use` method cannot infer correct return types.
In order to improve intellisense for the `use` method. You will have to define a type for the mappings too.
```ts
type MailerMappings = {
transactional: Mailgun
promotional: Mailgun
}
type MailerConfig = {
default: keyof MailerMappings
list: {
[K in keyof MailerMappings]: any
}
}
const mailerConfig: MailerConfig = {
default: 'transactional',
list: {
transactional: {
driver: 'mailgun',
// ...
},
promotional: {
driver: 'mailgun',
// ...
},
},
}
```
Finally, pass the `MailerMappings` to the Base Manager class
```ts
import { DriverContract, MailerMappings } from './Contracts'
class MailManager implements Manager<DriverContract, DriverContract, MailerMappings> {}
```
Once mailer mappings have been defined, the `use` method will have proper return types.
### Mapping suggestions
![](assets/mappings.png)
### Return type
![](assets/return-type.png)
[gh-workflow-image]: https://img.shields.io/github/workflow/status/poppinss/manager/test?style=for-the-badge
[gh-workflow-url]: https://github.com/poppinss/manager/actions/workflows/test.yml "Github action"
[typescript-image]: https://img.shields.io/badge/Typescript-294E80.svg?style=for-the-badge&logo=typescript
[typescript-url]: "typescript"
[npm-image]: https://img.shields.io/npm/v/@poppinss/manager.svg?style=for-the-badge&logo=npm
[npm-url]: https://npmjs.org/package/@poppinss/manager 'npm'
[license-image]: https://img.shields.io/npm/l/@poppinss/manager?color=blueviolet&style=for-the-badge
[license-url]: LICENSE.md 'license'
[synk-image]: https://img.shields.io/snyk/vulnerabilities/github/poppinss/manager?label=Synk%20Vulnerabilities&style=for-the-badge
[synk-url]: https://snyk.io/test/github/poppinss/manager?targetFile=package.json 'synk'
+2
View File
@@ -0,0 +1,2 @@
export { Manager } from './src/Manager';
export * from './src/Contracts';
+28
View File
@@ -0,0 +1,28 @@
"use strict";
/*
* @poppinss/manager
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Manager = void 0;
var Manager_1 = require("./src/Manager");
Object.defineProperty(exports, "Manager", { enumerable: true, get: function () { return Manager_1.Manager; } });
__exportStar(require("./src/Contracts"), exports);
+51
View File
@@ -0,0 +1,51 @@
/**
* Shape of the extend callback
*/
export declare type ExtendCallback<Manager extends ManagerContract<any, any>, Driver extends any, Name extends any> = (manager: Manager, mappingName: Name, config: any) => Driver;
/**
* Manager class shape
*/
export interface ManagerContract<Application extends any, DriverContract extends any, MappingValue extends any = DriverContract, MappingsList extends {
[key: string]: MappingValue;
} = any> {
application: Application;
/**
* Returns concrete type when binding name is from the mappings list
*/
use<K extends keyof MappingsList>(name: K): MappingsList[K];
/**
* Return a overload of mapping when no key is defined
*/
use(): {
[K in keyof MappingsList]: MappingsList[K];
}[keyof MappingsList];
/**
* Extend by adding a new custom driver
*/
extend(name: string, callback: ExtendCallback<this, DriverContract, keyof MappingsList>): void;
/**
* Release bindings from cache
*/
release<K extends keyof MappingsList>(name: K): void;
release(name: string): void;
}
/**
* Extracts and builds a list of mappings implementation
*/
export declare type ExtractImplementations<List extends {
[key: string]: {
implementation: any;
};
}> = {
[P in keyof List]: List[P]['implementation'];
};
/**
* Extracts and builds a list of mappings implementation
*/
export declare type ExtractConfig<List extends {
[key: string]: {
config: any;
};
}> = {
[P in keyof List]: List[P]['config'];
};
+10
View File
@@ -0,0 +1,10 @@
"use strict";
/*
* @poppinss/manager
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
Object.defineProperty(exports, "__esModule", { value: true });
+92
View File
@@ -0,0 +1,92 @@
import { ManagerContract, ExtendCallback } from './Contracts';
/**
* Manager class implements the Builder pattern to make instance of similar
* implementations using a fluent API vs importing each class by hand.
*
* This module is used extensively in AdonisJs. For example: `Mail`, `Sessions`,
* `Auth` and so on.
*/
export declare abstract class Manager<Application extends any, DriverContract extends any, MappingValue extends any = DriverContract, MappingsList extends {
[key: string]: MappingValue;
} = any> implements ManagerContract<Application, DriverContract, MappingValue, MappingsList> {
application: Application;
/**
* Mappings cache (if caching is enabled)
*/
private mappingsCache;
/**
* A cache to store the function names for initiating driver instances.
*/
private driverCreatorNames;
/**
* List of drivers added at runtime
*/
private extendedDrivers;
/**
* Whether or not to cache mappings
*/
protected abstract singleton: boolean;
/**
* Getting the default mapping name, incase a mapping
* is not defined
*/
protected abstract getDefaultMappingName(): keyof MappingsList;
/**
* Getting config for the mapping. It is required for making
* extended drivers
*/
protected abstract getMappingConfig(mappingName: keyof MappingsList): any | undefined;
/**
* Getting the driver name for the mapping
*/
protected abstract getMappingDriver(mappingName: keyof MappingsList): string | undefined;
constructor(application: Application);
/**
* Returns the value saved inside cache, this method will check for
* `cacheDrivers` attribute before entertaining the cache
*/
private getFromCache;
/**
* Saves value to the cache with the driver name. This method will check for
* `cacheDrivers` attribute before entertaining the cache.
*/
private saveToCache;
/**
* Make the extended driver instance and save it to cache (if enabled)
*/
private makeExtendedDriver;
/**
* Returns the creator function name for a given driver.
*/
private getDriverCreatorName;
/**
* Make the custom driver instance by checking for function on the
* parent class.
*
* For example: `stmp` as the driver name will look for `createSmtp`
* method on the parent class.
*/
private makeDriver;
/**
* Optional method to wrap the driver response
*/
protected wrapDriverResponse(_: keyof MappingsList, value: DriverContract): MappingValue;
/**
* Returns the instance of a given driver. If `name` is not defined
* the default driver will be resolved.
*/
use<K extends keyof MappingsList & string>(name: K): MappingsList[K];
use(): {
[K in keyof MappingsList]: MappingsList[K];
}[keyof MappingsList];
/**
* Removes the mapping from internal cache.
*/
release<K extends keyof MappingsList & string>(name: K): void;
release(name: string): void;
/**
* Extend by adding new driver. The compositon of driver
* is the responsibility of the callback function
*/
extend(name: string, callback: ExtendCallback<this, DriverContract, keyof MappingsList>): void;
}
+137
View File
@@ -0,0 +1,137 @@
"use strict";
/*
* @poppinss/manager
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.Manager = void 0;
/**
* The simplest implementation of capitalizing a string
*/
const capitalize = (value) => {
if (!value) {
return value;
}
return value.charAt(0).toUpperCase() + value.slice(1);
};
/**
* Manager class implements the Builder pattern to make instance of similar
* implementations using a fluent API vs importing each class by hand.
*
* This module is used extensively in AdonisJs. For example: `Mail`, `Sessions`,
* `Auth` and so on.
*/
class Manager {
constructor(application) {
this.application = application;
/**
* Mappings cache (if caching is enabled)
*/
this.mappingsCache = new Map();
/**
* A cache to store the function names for initiating driver instances.
*/
this.driverCreatorNames = new Map();
/**
* List of drivers added at runtime
*/
this.extendedDrivers = {};
}
/**
* Returns the value saved inside cache, this method will check for
* `cacheDrivers` attribute before entertaining the cache
*/
getFromCache(name) {
return this.mappingsCache.get(name) || null;
}
/**
* Saves value to the cache with the driver name. This method will check for
* `cacheDrivers` attribute before entertaining the cache.
*/
saveToCache(name, value) {
if (this.singleton) {
this.mappingsCache.set(name, value);
}
}
/**
* Make the extended driver instance and save it to cache (if enabled)
*/
makeExtendedDriver(mappingName, driver, config) {
const value = this.wrapDriverResponse(mappingName, this.extendedDrivers[driver](this, mappingName, config));
this.saveToCache(mappingName, value);
return value;
}
/**
* Returns the creator function name for a given driver.
*/
getDriverCreatorName(driver) {
if (!this.driverCreatorNames.has(driver)) {
this.driverCreatorNames.set(driver, `create${capitalize(driver.replace(/-\w|_\w/g, (g) => g.substr(1).toUpperCase()))}`);
}
return this.driverCreatorNames.get(driver);
}
/**
* Make the custom driver instance by checking for function on the
* parent class.
*
* For example: `stmp` as the driver name will look for `createSmtp`
* method on the parent class.
*/
makeDriver(mappingName, driver, config) {
const driverCreatorName = this.getDriverCreatorName(driver);
/**
* Raise error when the parent class doesn't implement the function
*/
if (typeof this[driverCreatorName] !== 'function') {
throw new Error(`"${driver}" driver is not supported by "${this.constructor.name}"`);
}
const value = this.wrapDriverResponse(mappingName, this[driverCreatorName](mappingName, config));
this.saveToCache(mappingName, value);
return value;
}
/**
* Optional method to wrap the driver response
*/
wrapDriverResponse(_, value) {
return value;
}
use(name) {
const mappingName = name || this.getDefaultMappingName();
const cached = this.getFromCache(mappingName);
if (cached) {
return cached;
}
/**
* Ensure that driver exists for a given mapping
*/
const driver = this.getMappingDriver(mappingName);
if (!driver) {
throw new Error(`Make sure to define driver for "${mappingName}" mapping`);
}
/**
* Making the extended driver
*/
if (this.extendedDrivers[driver]) {
return this.makeExtendedDriver(mappingName, driver, this.getMappingConfig(mappingName));
}
/**
* Making the predefined driver
*/
return this.makeDriver(mappingName, driver, this.getMappingConfig(mappingName));
}
release(name) {
this.mappingsCache.delete(name);
}
/**
* Extend by adding new driver. The compositon of driver
* is the responsibility of the callback function
*/
extend(name, callback) {
this.extendedDrivers[name] = callback;
}
}
exports.Manager = Manager;
+125
View File
@@ -0,0 +1,125 @@
{
"name": "@poppinss/manager",
"version": "5.0.2",
"description": "The builder (Manager) pattern implementation",
"main": "build/index.js",
"files": [
"build/src",
"build/index.d.ts",
"build/index.js"
],
"scripts": {
"mrm": "mrm --preset=@adonisjs/mrm-preset",
"pretest": "npm run lint",
"test": "node .bin/test.js",
"clean": "del build",
"compile": "npm run lint && npm run clean && tsc",
"build": "npm run compile",
"commit": "git-cz",
"release": "np --message=\"chore(release): %s\"",
"version": "npm run build",
"format": "prettier --write .",
"prepublishOnly": "npm run build",
"lint": "eslint . --ext=.ts",
"sync-labels": "github-label-sync --labels ./node_modules/@adonisjs/mrm-preset/gh-labels.json poppinss/manager"
},
"keywords": [
"builder-pattern",
"adonisjs"
],
"author": "virk,poppinss",
"license": "MIT",
"devDependencies": {
"@adonisjs/mrm-preset": "^5.0.3",
"@adonisjs/require-ts": "^2.0.11",
"@types/node": "^17.0.23",
"commitizen": "^4.2.4",
"cz-conventional-changelog": "^3.3.0",
"del-cli": "^4.0.1",
"doctoc": "^2.0.1",
"eslint": "^8.12.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-adonis": "^2.1.0",
"eslint-plugin-prettier": "^4.0.0",
"github-label-sync": "^2.2.0",
"husky": "^7.0.1",
"japa": "^4.0.0",
"mrm": "^4.0.0",
"np": "^7.6.1",
"prettier": "^2.6.2",
"typescript": "^4.6.3"
},
"nyc": {
"exclude": [
"test"
],
"extension": [
".ts"
]
},
"config": {
"commitizen": {
"path": "cz-conventional-changelog"
}
},
"np": {
"contents": ".",
"anyBranch": false
},
"directories": {
"doc": "docs",
"example": "example",
"test": "test"
},
"repository": {
"type": "git",
"url": "git+https://github.com/poppinss/manager.git"
},
"bugs": {
"url": "https://github.com/poppinss/manager/issues"
},
"homepage": "https://github.com/poppinss/manager#readme",
"mrmConfig": {
"core": false,
"license": "MIT",
"services": [
"github-actions"
],
"minNodeVersion": "16.13.1",
"probotApps": [
"stale",
"lock"
],
"runGhActionsOnWindows": false
},
"eslintConfig": {
"extends": [
"plugin:adonis/typescriptPackage",
"prettier"
],
"plugins": [
"prettier"
],
"rules": {
"prettier/prettier": [
"error",
{
"endOfLine": "auto"
}
]
}
},
"eslintIgnore": [
"build"
],
"prettier": {
"trailingComma": "es5",
"semi": false,
"singleQuote": true,
"useTabs": false,
"quoteProps": "consistent",
"bracketSpacing": true,
"arrowParens": "always",
"printWidth": 100
}
}