Added ":" syntax to adapter manager

refs https://github.com/TryGhost/Toolbox/issues/384

- The syntax using a colon ":" separator has been successfully used to enable multiple adapters. The adapter manager can benefit from same convention to enable more elastic adapter cache - have multiple instances of adapters from same base class
This commit is contained in:
Naz 2022-09-06 12:05:59 +08:00
parent 2f2033e7c5
commit 0f86a05ed4
No known key found for this signature in database
2 changed files with 69 additions and 10 deletions

View File

@ -56,19 +56,26 @@ module.exports = class AdapterManager {
/**
* getAdapter
*
* @param {string} adapterType The type of adapter, e.g. "storage" or "scheduling"
* @param {string} adapterName The active adapter, e.g. "LocalFileStorage"
* @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"
* @param {object} [config] The config the adapter could be instantiated with
*
* @returns {Adapter} The resolved and instantiated adapter
*/
getAdapter(adapterType, adapterName, config) {
if (!adapterType || !adapterName) {
getAdapter(adapterName, adapterClassName, config) {
if (!adapterName || !adapterClassName) {
throw new errors.IncorrectUsageError({
message: 'getAdapter must be called with a adapterType and a name.'
message: 'getAdapter must be called with a adapterName and a adapterClassName.'
});
}
let adapterType;
if (adapterName.includes(':')) {
[adapterType] = adapterName.split(':');
} else {
adapterType = adapterName;
}
const adapterCache = this.instanceCache[adapterType];
if (!adapterCache) {
@ -84,7 +91,7 @@ module.exports = class AdapterManager {
/** @type AdapterConstructor */
let Adapter;
for (const pathToAdapters of this.pathsToAdapters) {
const pathToAdapter = path.join(pathToAdapters, adapterType, adapterName);
const pathToAdapter = path.join(pathToAdapters, adapterType, adapterClassName);
try {
Adapter = this.loadAdapterFromPath(pathToAdapter);
if (Adapter) {
@ -108,7 +115,7 @@ module.exports = class AdapterManager {
if (!Adapter) {
throw new errors.IncorrectUsageError({
message: `Unable to find ${adapterType} adapter ${adapterName} in ${this.pathsToAdapters}.`
message: `Unable to find ${adapterType} adapter ${adapterClassName} in ${this.pathsToAdapters}.`
});
}
@ -117,21 +124,21 @@ module.exports = class AdapterManager {
if (!(adapter instanceof this.baseClasses[adapterType])) {
if (Object.getPrototypeOf(Adapter).name !== this.baseClasses[adapterType].name) {
throw new errors.IncorrectUsageError({
message: `${adapterType} adapter ${adapterName} does not inherit from the base class.`
message: `${adapterType} adapter ${adapterClassName} does not inherit from the base class.`
});
}
}
if (!adapter.requiredFns) {
throw new errors.IncorrectUsageError({
message: `${adapterType} adapter ${adapterName} does not have the requiredFns.`
message: `${adapterType} adapter ${adapterClassName} 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.`
message: `${adapterType} adapter ${adapterClassName} is missing the ${requiredFn} method.`
});
}
}

View File

@ -19,6 +19,29 @@ class DefaultMailAdapter extends BaseMailAdapter {
}
describe('AdapterManager', function () {
it('getAdapter throws if called without correct parameters', function () {
const pathsToAdapters = [
'first/path'
];
const loadAdapterFromPath = sinon.stub();
const adapterManager = new AdapterManager({
loadAdapterFromPath,
pathsToAdapters
});
adapterManager.registerAdapter('mail', BaseMailAdapter);
try {
adapterManager.getAdapter('mail');
should.fail(null, null, 'Should not have created');
} catch (err) {
should.exist(err);
should.equal(err.errorType, 'IncorrectUsageError');
should.equal(err.message, 'getAdapter must be called with a adapterName and a adapterClassName.');
}
});
it('Loads registered adapters in the order defined by the paths', function () {
const pathsToAdapters = [
'first/path',
@ -58,6 +81,7 @@ describe('AdapterManager', function () {
} catch (err) {
should.exist(err);
should.equal(err.errorType, 'IncorrectUsageError');
should.equal(err.message, 'mail adapter incomplete is missing the someMethod method.');
}
try {
@ -77,4 +101,32 @@ describe('AdapterManager', function () {
should.equal(err.errorType, 'IncorrectUsageError');
}
});
it('Reads adapter type from the adapter name divided with a colon (adapter:feature syntax)', function () {
const pathsToAdapters = [
'/path'
];
const loadAdapterFromPath = sinon.stub();
loadAdapterFromPath.withArgs('/path/mail/custom')
.returns(CustomMailAdapter);
loadAdapterFromPath.withArgs('/path/mail/default')
.returns(DefaultMailAdapter);
const adapterManager = new AdapterManager({
loadAdapterFromPath,
pathsToAdapters
});
adapterManager.registerAdapter('mail', BaseMailAdapter);
try {
const customAdapter = adapterManager.getAdapter('mail:newsletters', 'custom');
should.ok(customAdapter instanceof BaseMailAdapter);
should.ok(customAdapter instanceof CustomMailAdapter);
} catch (err) {
should.fail(err, null, 'Should not have errored');
}
});
});