mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-22 19:32:54 +03:00
parent
4990c05448
commit
44db45b7c3
6
ghost/adapter-manager/.eslintrc.js
Normal file
6
ghost/adapter-manager/.eslintrc.js
Normal file
@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: ['ghost'],
|
||||
extends: [
|
||||
'plugin:ghost/node',
|
||||
]
|
||||
};
|
21
ghost/adapter-manager/LICENSE
Normal file
21
ghost/adapter-manager/LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2013-2020 Ghost Foundation
|
||||
|
||||
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.
|
62
ghost/adapter-manager/README.md
Normal file
62
ghost/adapter-manager/README.md
Normal file
@ -0,0 +1,62 @@
|
||||
# Adapter Manager
|
||||
|
||||
A manager for retrieving custom "adapters" - can be used to abstract away from custom implementations
|
||||
|
||||
## Install
|
||||
|
||||
`npm install @tryghost/adapter-manager --save`
|
||||
|
||||
or
|
||||
|
||||
`yarn add @tryghost/adapter-manager`
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
```js
|
||||
const AdapterManager = require('@tryghost/adapter-manager');
|
||||
|
||||
const adapterManager = new AdapterManager({
|
||||
pathsToAdapters: [
|
||||
'/path/to/custom/adapters',
|
||||
'/path/to/default/adapters'
|
||||
]
|
||||
});
|
||||
|
||||
class MailAdapterBase {
|
||||
someMethod() {}
|
||||
}
|
||||
|
||||
adapterManager.register('mail', MailAdapterBase);
|
||||
|
||||
const mailAdapterInstance = adapterManager.getAdapter('mail', 'direct', mailConfig);
|
||||
|
||||
mailAdapterInstance.someMethod();
|
||||
```
|
||||
|
||||
|
||||
## Develop
|
||||
|
||||
This is a mono repository, managed with [lerna](https://lernajs.io/).
|
||||
|
||||
Follow the instructions for the top-level repo.
|
||||
1. `git clone` this repo & `cd` into it as usual
|
||||
2. Run `yarn` to install top-level dependencies.
|
||||
|
||||
|
||||
## Run
|
||||
|
||||
- `yarn dev`
|
||||
|
||||
|
||||
## Test
|
||||
|
||||
- `yarn lint` run just eslint
|
||||
- `yarn test` run lint and tests
|
||||
|
||||
|
||||
|
||||
|
||||
# Copyright & License
|
||||
|
||||
Copyright (c) 2020 Ghost Foundation - Released under the [MIT license](LICENSE).
|
1
ghost/adapter-manager/index.js
Normal file
1
ghost/adapter-manager/index.js
Normal file
@ -0,0 +1 @@
|
||||
module.exports = require('./lib/AdapterManager');
|
138
ghost/adapter-manager/lib/AdapterManager.js
Normal file
138
ghost/adapter-manager/lib/AdapterManager.js
Normal file
@ -0,0 +1,138 @@
|
||||
const path = require('path');
|
||||
const errors = require('@tryghost/errors');
|
||||
|
||||
/**
|
||||
* @typedef { function(new: Adapter, object) } AdapterConstructor
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} Adapter
|
||||
* @prop {string[]} requiredFns
|
||||
*/
|
||||
|
||||
module.exports = class AdapterManager {
|
||||
/**
|
||||
* @param {object} config
|
||||
* @param {string[]} config.pathsToAdapters The paths to check, e.g. ['content/adapters', 'core/server/adapters']
|
||||
* @param {(path: string) => AdapterConstructor} config.loadAdapterFromPath A function to load adapters, e.g. global.require
|
||||
*/
|
||||
constructor({pathsToAdapters, loadAdapterFromPath}) {
|
||||
/**
|
||||
* @private
|
||||
* @type {Object.<string, AdapterConstructor>}
|
||||
*/
|
||||
this.baseClasses = {};
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @type {Object.<string, Object.<string, Adapter>>}
|
||||
*/
|
||||
this.instanceCache = {};
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @type {string[]}
|
||||
*/
|
||||
this.pathsToAdapters = pathsToAdapters;
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @type {(path: string) => AdapterConstructor}
|
||||
*/
|
||||
this.loadAdapterFromPath = loadAdapterFromPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register an adapter type and the corresponding base class. Must be called before requesting adapters of that type
|
||||
*
|
||||
* @param {string} type The name for the type of adapter
|
||||
* @param {AdapterConstructor} BaseClass The class from which all adapters of this type must extend
|
||||
*/
|
||||
registerAdapter(type, BaseClass) {
|
||||
this.instanceCache[type] = {};
|
||||
this.baseClasses[type] = BaseClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* getAdapter
|
||||
*
|
||||
* @param {string} adapterType The type of adapter, e.g. "storage" or "scheduling"
|
||||
* @param {string} adapterName The active adapter, e.g. "LocalFileStorage"
|
||||
* @param {object} config The config the adapter should be instantiated with
|
||||
*
|
||||
* @returns {Adapter} The resolved and instantiated adapter
|
||||
*/
|
||||
getAdapter(adapterType, adapterName, config) {
|
||||
if (!adapterType || !adapterName) {
|
||||
throw new errors.IncorrectUsageError({
|
||||
message: 'getAdapter must be called with a adapterType and a name.'
|
||||
});
|
||||
}
|
||||
|
||||
const adapterCache = this.instanceCache[adapterType];
|
||||
|
||||
if (!adapterCache) {
|
||||
throw new errors.NotFoundError({
|
||||
message: `Unknown adapter type ${adapterType}. Please register adapter.`
|
||||
});
|
||||
}
|
||||
|
||||
if (adapterCache[adapterName]) {
|
||||
return adapterCache[adapterName];
|
||||
}
|
||||
|
||||
/** @type AdapterConstructor */
|
||||
let Adapter;
|
||||
for (const pathToAdapters of this.pathsToAdapters) {
|
||||
const pathToAdapter = path.join(pathToAdapters, adapterType, adapterName);
|
||||
try {
|
||||
Adapter = this.loadAdapterFromPath(pathToAdapter);
|
||||
if (Adapter) {
|
||||
break;
|
||||
}
|
||||
} catch (err) {
|
||||
// Catch runtime errors
|
||||
if (err.code !== 'MODULE_NOT_FOUND') {
|
||||
throw new errors.IncorrectUsageError({err});
|
||||
}
|
||||
|
||||
// Catch missing dependencies BUT NOT missing adapter
|
||||
if (!err.message.includes(pathToAdapter)) {
|
||||
throw new errors.IncorrectUsageError({err});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!Adapter) {
|
||||
throw new errors.IncorrectUsageError({
|
||||
message: `Unable to find ${adapterType} adapter ${adapterName} in ${this.pathsToAdapters}.`
|
||||
});
|
||||
}
|
||||
|
||||
const adapter = new Adapter(config);
|
||||
|
||||
if (!(adapter instanceof this.baseClasses[adapterType])) {
|
||||
throw new errors.IncorrectUsageError({
|
||||
message: `${adapterType} adapter ${adapterName} does not inherit from the base class.`
|
||||
});
|
||||
}
|
||||
|
||||
if (!adapter.requiredFns) {
|
||||
throw new errors.IncorrectUsageError({
|
||||
message: `${adapterType} adapter ${adapterName} does not have the requiredFns.`
|
||||
});
|
||||
}
|
||||
|
||||
for (const requiredFn of adapter.requiredFns) {
|
||||
if (typeof adapter[requiredFn] !== 'function') {
|
||||
throw new errors.IncorrectUsageError({
|
||||
message: `${adapterType} adapter ${adapterName} is missing the ${requiredFn} method.`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
adapterCache[adapterName] = adapter;
|
||||
|
||||
return adapter;
|
||||
}
|
||||
};
|
35
ghost/adapter-manager/package.json
Normal file
35
ghost/adapter-manager/package.json
Normal file
@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "@tryghost/adapter-manager",
|
||||
"version": "0.0.0",
|
||||
"repository": "https://github.com/TryGhost/Ghost-Utils/tree/master/packages/adapter-manager",
|
||||
"author": "Ghost Foundation",
|
||||
"license": "MIT",
|
||||
"main": "index.js",
|
||||
"types": "types/index.d.ts",
|
||||
"scripts": {
|
||||
"dev": "echo \"Implement me!\"",
|
||||
"test": "NODE_ENV=testing mocha './test/**/*.test.js'",
|
||||
"lint": "eslint . --ext .js --cache",
|
||||
"posttest": "yarn lint",
|
||||
"pretest": "yarn types",
|
||||
"types": "rm -r types && yarn tsc"
|
||||
},
|
||||
"files": [
|
||||
"index.js",
|
||||
"lib"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/mocha": "^7.0.2",
|
||||
"@types/sinon": "^9.0.0",
|
||||
"mocha": "7.1.1",
|
||||
"should": "13.2.3",
|
||||
"sinon": "9.0.1",
|
||||
"typescript": "^3.8.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tryghost/errors": "^0.1.1"
|
||||
}
|
||||
}
|
6
ghost/adapter-manager/test/.eslintrc.js
Normal file
6
ghost/adapter-manager/test/.eslintrc.js
Normal file
@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: ['ghost'],
|
||||
extends: [
|
||||
'plugin:ghost/test',
|
||||
]
|
||||
};
|
80
ghost/adapter-manager/test/AdapterManager.test.js
Normal file
80
ghost/adapter-manager/test/AdapterManager.test.js
Normal file
@ -0,0 +1,80 @@
|
||||
const sinon = require('sinon');
|
||||
const should = require('should');
|
||||
const AdapterManager = require('../');
|
||||
|
||||
class BaseMailAdapter {
|
||||
constructor() {
|
||||
this.requiredFns = ['someMethod'];
|
||||
}
|
||||
}
|
||||
|
||||
class IncompleteMailAdapter extends BaseMailAdapter {}
|
||||
|
||||
class CustomMailAdapter extends BaseMailAdapter {
|
||||
someMethod() {}
|
||||
}
|
||||
|
||||
class DefaultMailAdapter extends BaseMailAdapter {
|
||||
someMethod() {}
|
||||
}
|
||||
|
||||
describe('AdapterManager', function () {
|
||||
it('Loads registered adapters in the order defined by the paths', function () {
|
||||
const pathsToAdapters = [
|
||||
'first/path',
|
||||
'second/path',
|
||||
'third/path'
|
||||
];
|
||||
|
||||
const loadAdapterFromPath = sinon.stub();
|
||||
loadAdapterFromPath.withArgs('first/path/mail/incomplete')
|
||||
.returns(IncompleteMailAdapter);
|
||||
loadAdapterFromPath.withArgs('second/path/mail/custom')
|
||||
.returns(CustomMailAdapter);
|
||||
loadAdapterFromPath.withArgs('third/path/mail/default')
|
||||
.returns(DefaultMailAdapter);
|
||||
loadAdapterFromPath.withArgs('first/path/mail/broken')
|
||||
.throwsException('SHIT_GOT_REAL');
|
||||
|
||||
const adapterManager = new AdapterManager({
|
||||
loadAdapterFromPath,
|
||||
pathsToAdapters
|
||||
});
|
||||
|
||||
adapterManager.registerAdapter('mail', BaseMailAdapter);
|
||||
|
||||
try {
|
||||
const customAdapter = adapterManager.getAdapter('mail', 'custom', {});
|
||||
|
||||
should.ok(customAdapter instanceof BaseMailAdapter);
|
||||
should.ok(customAdapter instanceof CustomMailAdapter);
|
||||
} catch (err) {
|
||||
should.fail(err, null, 'Should not have errored');
|
||||
}
|
||||
|
||||
try {
|
||||
const incompleteAdapter = adapterManager.getAdapter('mail', 'incomplete', {});
|
||||
should.fail(incompleteAdapter, null, 'Should not have created');
|
||||
} catch (err) {
|
||||
should.exist(err);
|
||||
should.equal(err.errorType, 'IncorrectUsageError');
|
||||
}
|
||||
|
||||
try {
|
||||
const defaultAdapter = adapterManager.getAdapter('mail', 'default', {});
|
||||
|
||||
should.ok(defaultAdapter instanceof BaseMailAdapter);
|
||||
should.ok(defaultAdapter instanceof DefaultMailAdapter);
|
||||
} catch (err) {
|
||||
should.fail(err, null, 'Should not have errored');
|
||||
}
|
||||
|
||||
try {
|
||||
const brokenAdapter = adapterManager.getAdapter('mail', 'broken', {});
|
||||
should.fail(brokenAdapter, null, 'Should not have created');
|
||||
} catch (err) {
|
||||
should.exist(err);
|
||||
should.equal(err.errorType, 'IncorrectUsageError');
|
||||
}
|
||||
});
|
||||
});
|
15
ghost/adapter-manager/tsconfig.json
Normal file
15
ghost/adapter-manager/tsconfig.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"emitDeclarationOnly": true,
|
||||
"outDir": "types",
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"target": "es6"
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
2
ghost/adapter-manager/types/index.d.ts
vendored
Normal file
2
ghost/adapter-manager/types/index.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
declare const _exports: typeof import("./lib/AdapterManager");
|
||||
export = _exports;
|
56
ghost/adapter-manager/types/lib/AdapterManager.d.ts
vendored
Normal file
56
ghost/adapter-manager/types/lib/AdapterManager.d.ts
vendored
Normal file
@ -0,0 +1,56 @@
|
||||
export = AdapterManager;
|
||||
declare class AdapterManager {
|
||||
/**
|
||||
* @param {object} config
|
||||
* @param {string[]} config.pathsToAdapters The paths to check, e.g. ['content/adapters', 'core/server/adapters']
|
||||
* @param {(path: string) => AdapterConstructor} config.loadAdapterFromPath A function to load adapters, e.g. global.require
|
||||
*/
|
||||
constructor({ pathsToAdapters, loadAdapterFromPath }: {
|
||||
pathsToAdapters: string[];
|
||||
loadAdapterFromPath: (path: string) => new (arg1: any) => Adapter;
|
||||
});
|
||||
/**
|
||||
* @private
|
||||
* @type {Object.<string, AdapterConstructor>}
|
||||
*/
|
||||
private baseClasses;
|
||||
/**
|
||||
* @private
|
||||
* @type {Object.<string, Object.<string, Adapter>>}
|
||||
*/
|
||||
private instanceCache;
|
||||
/**
|
||||
* @private
|
||||
* @type {string[]}
|
||||
*/
|
||||
private pathsToAdapters;
|
||||
/**
|
||||
* @private
|
||||
* @type {(path: string) => AdapterConstructor}
|
||||
*/
|
||||
private loadAdapterFromPath;
|
||||
/**
|
||||
* Register an adapter type and the corresponding base class. Must be called before requesting adapters of that type
|
||||
*
|
||||
* @param {string} type The name for the type of adapter
|
||||
* @param {AdapterConstructor} BaseClass The class from which all adapters of this type must extend
|
||||
*/
|
||||
registerAdapter(type: string, BaseClass: new (arg1: any) => Adapter): void;
|
||||
/**
|
||||
* getAdapter
|
||||
*
|
||||
* @param {string} adapterType The type of adapter, e.g. "storage" or "scheduling"
|
||||
* @param {string} adapterName The active adapter, e.g. "LocalFileStorage"
|
||||
* @param {object} config The config the adapter should be instantiated with
|
||||
*
|
||||
* @returns {Adapter} The resolved and instantiated adapter
|
||||
*/
|
||||
getAdapter(adapterType: string, adapterName: string, config: any): Adapter;
|
||||
}
|
||||
declare namespace AdapterManager {
|
||||
export { AdapterConstructor, Adapter };
|
||||
}
|
||||
type Adapter = {
|
||||
requiredFns: string[];
|
||||
};
|
||||
type AdapterConstructor = new (arg1: any) => Adapter;
|
1
ghost/adapter-manager/types/test/AdapterManager.test.d.ts
vendored
Normal file
1
ghost/adapter-manager/types/test/AdapterManager.test.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export {};
|
Loading…
Reference in New Issue
Block a user