2020-04-05 16:54:47 +03:00
|
|
|
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
|
|
|
|
*
|
2022-09-06 07:05:59 +03:00
|
|
|
* @param {string} adapterName The name of the type of adapter, e.g. "storage" or "scheduling", optionally including the feature, e.g. "storage:files"
|
|
|
|
* @param {string} adapterClassName The active adapter instance class name e.g. "LocalFileStorage"
|
2022-09-06 04:54:42 +03:00
|
|
|
* @param {object} [config] The config the adapter could be instantiated with
|
2020-04-05 16:54:47 +03:00
|
|
|
*
|
|
|
|
* @returns {Adapter} The resolved and instantiated adapter
|
|
|
|
*/
|
2022-09-06 07:05:59 +03:00
|
|
|
getAdapter(adapterName, adapterClassName, config) {
|
|
|
|
if (!adapterName || !adapterClassName) {
|
2020-04-05 16:54:47 +03:00
|
|
|
throw new errors.IncorrectUsageError({
|
2022-09-06 07:05:59 +03:00
|
|
|
message: 'getAdapter must be called with a adapterName and a adapterClassName.'
|
2020-04-05 16:54:47 +03:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2022-09-06 07:05:59 +03:00
|
|
|
let adapterType;
|
|
|
|
if (adapterName.includes(':')) {
|
|
|
|
[adapterType] = adapterName.split(':');
|
|
|
|
} else {
|
|
|
|
adapterType = adapterName;
|
|
|
|
}
|
|
|
|
|
2020-04-05 16:54:47 +03:00
|
|
|
const adapterCache = this.instanceCache[adapterType];
|
|
|
|
|
|
|
|
if (!adapterCache) {
|
|
|
|
throw new errors.NotFoundError({
|
|
|
|
message: `Unknown adapter type ${adapterType}. Please register adapter.`
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2022-09-06 07:35:07 +03:00
|
|
|
// @NOTE: example cache key value 'email:newsletters:custom-newsletter-adapter'
|
|
|
|
const adapterCacheKey = `${adapterName}:${adapterClassName}`;
|
|
|
|
if (adapterCache[adapterCacheKey]) {
|
|
|
|
return adapterCache[adapterCacheKey];
|
2020-04-05 16:54:47 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
/** @type AdapterConstructor */
|
|
|
|
let Adapter;
|
|
|
|
for (const pathToAdapters of this.pathsToAdapters) {
|
2022-09-06 07:05:59 +03:00
|
|
|
const pathToAdapter = path.join(pathToAdapters, adapterType, adapterClassName);
|
2020-04-05 16:54:47 +03:00
|
|
|
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)) {
|
2020-04-05 17:30:48 +03:00
|
|
|
throw new errors.IncorrectUsageError({
|
|
|
|
message: `You are missing dependencies in your adapter ${pathToAdapter}`,
|
|
|
|
err
|
|
|
|
});
|
2020-04-05 16:54:47 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!Adapter) {
|
|
|
|
throw new errors.IncorrectUsageError({
|
2022-09-06 07:05:59 +03:00
|
|
|
message: `Unable to find ${adapterType} adapter ${adapterClassName} in ${this.pathsToAdapters}.`
|
2020-04-05 16:54:47 +03:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
const adapter = new Adapter(config);
|
|
|
|
|
|
|
|
if (!(adapter instanceof this.baseClasses[adapterType])) {
|
2020-04-07 16:24:44 +03:00
|
|
|
if (Object.getPrototypeOf(Adapter).name !== this.baseClasses[adapterType].name) {
|
|
|
|
throw new errors.IncorrectUsageError({
|
2022-09-06 07:05:59 +03:00
|
|
|
message: `${adapterType} adapter ${adapterClassName} does not inherit from the base class.`
|
2020-04-07 16:24:44 +03:00
|
|
|
});
|
|
|
|
}
|
2020-04-05 16:54:47 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
if (!adapter.requiredFns) {
|
|
|
|
throw new errors.IncorrectUsageError({
|
2022-09-06 07:05:59 +03:00
|
|
|
message: `${adapterType} adapter ${adapterClassName} does not have the requiredFns.`
|
2020-04-05 16:54:47 +03:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const requiredFn of adapter.requiredFns) {
|
|
|
|
if (typeof adapter[requiredFn] !== 'function') {
|
|
|
|
throw new errors.IncorrectUsageError({
|
2022-09-06 07:05:59 +03:00
|
|
|
message: `${adapterType} adapter ${adapterClassName} is missing the ${requiredFn} method.`
|
2020-04-05 16:54:47 +03:00
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-09-06 07:35:07 +03:00
|
|
|
adapterCache[adapterCacheKey] = adapter;
|
2020-04-05 16:54:47 +03:00
|
|
|
|
|
|
|
return adapter;
|
|
|
|
}
|
|
|
|
};
|