mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-26 13:35:16 +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