Commit 64f00532 authored by Fabrizio Ruggeri's avatar Fabrizio Ruggeri
Browse files

Different approach to plugins.

Now the options can be passed externally if the instance is given
parent b2510f49
......@@ -17,10 +17,9 @@ const config = {} as Config;
describe('Domain Whitelist plugin', () => {
describe('plugin behavior', () => {
test('the factory returns a plugin', () => {
const plugin = domainWhitelistFactory({
const plugin = domainWhitelistFactory({})({
PLUGIN_IGNORE_RESULT: PLUGIN_IGNORE_RESULT,
config,
pluginOptions: {},
});
expect(plugin).toHaveProperty('getMiddlewares');
expect(plugin.getMiddlewares?.()).toHaveLength(1);
......@@ -28,14 +27,6 @@ describe('Domain Whitelist plugin', () => {
});
describe('middleware', () => {
// const plugin = domainWhitelistFactory({
// PLUGIN_IGNORE_RESULT: PLUGIN_IGNORE_RESULT,
// config,
// pluginOptions: {
// whitelist: ['caravaggio.com'],
// },
// });
// const middleware = plugin.getMiddlewares?.()[0];
const spy = jest.fn();
const req = ({
query: {
......@@ -49,10 +40,9 @@ describe('Domain Whitelist plugin', () => {
});
test('if no whitelist is provide, next is called', () => {
const plugin = domainWhitelistFactory({
const plugin = domainWhitelistFactory({})({
PLUGIN_IGNORE_RESULT: PLUGIN_IGNORE_RESULT,
config,
pluginOptions: {},
});
const middleware = plugin.getMiddlewares?.()[0];
middleware?.(spy)(req, res);
......@@ -61,11 +51,10 @@ describe('Domain Whitelist plugin', () => {
test('if whitelist is empty, next is called', () => {
const plugin = domainWhitelistFactory({
whitelist: [],
})({
PLUGIN_IGNORE_RESULT: PLUGIN_IGNORE_RESULT,
config,
pluginOptions: {
whitelist: [],
},
});
const middleware = plugin.getMiddlewares?.()[0];
middleware?.(spy)(req, res);
......@@ -74,11 +63,10 @@ describe('Domain Whitelist plugin', () => {
test('if no image is provided, next is called', () => {
const plugin = domainWhitelistFactory({
whitelist: ['anything.com'],
})({
PLUGIN_IGNORE_RESULT: PLUGIN_IGNORE_RESULT,
config,
pluginOptions: {
whitelist: ['anything.com'],
},
});
const middleware = plugin.getMiddlewares?.()[0];
const req = ({ query: {} } as unknown) as ServerRequest;
......@@ -88,11 +76,10 @@ describe('Domain Whitelist plugin', () => {
test('if image is not in domain, an error is thrown', () => {
const plugin = domainWhitelistFactory({
whitelist: ['anything.com'],
})({
PLUGIN_IGNORE_RESULT: PLUGIN_IGNORE_RESULT,
config,
pluginOptions: {
whitelist: ['anything.com'],
},
});
const middleware = plugin.getMiddlewares?.()[0];
const req = ({
......@@ -104,11 +91,10 @@ describe('Domain Whitelist plugin', () => {
test('if image is not in a valid url, next is called', () => {
const plugin = domainWhitelistFactory({
whitelist: ['anything.com'],
})({
PLUGIN_IGNORE_RESULT: PLUGIN_IGNORE_RESULT,
config,
pluginOptions: {
whitelist: ['anything.com'],
},
});
const middleware = plugin.getMiddlewares?.()[0];
const req = ({
......
......@@ -12,10 +12,9 @@ interface DomainWhitelistOptions {
whitelist?: Array<string>;
}
const domainWhitelistFactory: PluginConstructor<DomainWhitelistOptions> = ({
// PLUGIN_IGNORE_RESULT,
pluginOptions,
}) => {
const domainWhitelistFactory = (
pluginOptions: DomainWhitelistOptions
): PluginConstructor => () => {
const { whitelist = [] } = pluginOptions || {};
const validDomains = buildRegex(whitelist);
return {
......
import { PluginConstructor } from '../pluginLoader/pluginLoader';
import fetch from 'node-fetch';
const webImageLoaderFactory: PluginConstructor<{}> = ({
const webImageLoaderFactory = (): PluginConstructor => ({
PLUGIN_IGNORE_RESULT,
}) => {
return {
......
import { LoggerOptions, DestinationStream } from 'pino';
import { RawOperation } from '../utils/operationParser';
import type { PluginConstructor } from '../pluginLoader/pluginLoader';
interface CacheBaseConfig {
type: 'memory' | 'file' | 'none' | Function;
......@@ -151,16 +152,16 @@ export interface Config {
/**
* A series of path where to look for plugins
*/
paths: string[];
paths?: string[];
/**
* All the plugins
*/
plugins: {
[name: string]: {
disabled?: boolean;
options?: unknown;
};
};
plugins: Array<{
name: string;
instance?: PluginConstructor;
disabled?: boolean;
options?: unknown;
}>;
};
defaultOperations?: Array<RawOperation>;
......
......@@ -8,7 +8,7 @@ import pluginLoader from '../pluginLoader/pluginLoader';
import { ServerRequest } from 'microrouter';
const mockImageGet = jest.fn(async () => Buffer.from('an image buffer'));
jest.mock('../basePlugins/webImageLoader', () => (/* config */) => ({
jest.mock('../basePlugins/webImageLoader', () => () => (/* config */) => ({
inputImageLoader: mockImageGet,
}));
const config = {} as Config;
......
......@@ -33,7 +33,7 @@ const pipelineCreator = (context: Context): Pipeline => {
} = context;
return async ({ url, rawOperations, req }) => {
const buffer = await loader.get(url);
const buffer = await loader.get(url, req);
const image = sharp(buffer);
const operations = normalize([...defaultOperations, ...rawOperations]);
const result = await operations.reduce(
......
......@@ -2,13 +2,14 @@ import type { Config } from '../config/default';
import { tryEach } from '../utils/flow';
import webImageLoader from '../basePlugins/webImageLoader';
import { Logger } from 'pino';
import { AugmentedRequestHandler } from 'microrouter';
import { AugmentedRequestHandler, ServerRequest, options } from 'microrouter';
import domainWhitelistFactory from '../basePlugins/domainWhitelist';
import flatten from 'lodash/flatten';
export const PLUGIN_IGNORE_RESULT = Symbol('PLUGIN_IGNORE_RESULT');
export interface Plugin {
urlTransform?: (url: string, req: ServerRequest) => Promise<string>;
inputImageLoader?: (
imageUrl: string
) => Promise<Buffer | typeof PLUGIN_IGNORE_RESULT | null>;
......@@ -17,12 +18,15 @@ export interface Plugin {
>;
}
export type PluginConstructor<TPluginOptions> = (opt: {
export type PluginConstructor = (opt: {
config: Config;
pluginOptions?: TPluginOptions;
PLUGIN_IGNORE_RESULT: typeof PLUGIN_IGNORE_RESULT;
}) => Plugin;
// export type PluginConstructor<TPluginOptions = unknown> = (
// pOpt?: TPluginOptions
// ) => PluginSecondaryConstructor;
interface PluginDescriptor {
name: string;
instance: Plugin;
......@@ -34,37 +38,39 @@ const pluginLoader = (config: Config, logger?: Logger): PluginManager => {
let loadedPlugins: PluginDescriptor[] = [
{
name: 'webImageLoader',
instance: webImageLoader({ config, PLUGIN_IGNORE_RESULT }),
instance: webImageLoader()({ config, PLUGIN_IGNORE_RESULT }),
},
{
name: 'domainWhitelist',
instance: domainWhitelistFactory({
instance: domainWhitelistFactory({ whitelist: config.whitelist })({
config,
PLUGIN_IGNORE_RESULT,
pluginOptions: { whitelist: config.whitelist },
}),
},
];
if (config.plugins) {
const { paths, plugins = {} } = config.plugins;
loadedPlugins = Object.entries(plugins)
.filter(([, { disabled }]) => !disabled)
.reduce<PluginDescriptor[]>((acc, opt) => {
const [name, { options }] = opt;
// eslint-disable-next-line @typescript-eslint/no-var-requires
const plugin = require(require.resolve(name, {
paths,
})) as PluginConstructor<unknown>;
const { paths, plugins = [] } = config.plugins;
loadedPlugins = plugins
.filter((plugin) => !plugin.disabled)
.reduce<PluginDescriptor[]>((acc, { name, instance, options }) => {
let plugin: PluginConstructor;
if (instance) {
plugin = instance;
} else {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const loaded = require(require.resolve(name, {
paths,
}));
plugin = loaded(options);
}
return [
...acc,
{
name,
instance: plugin({
config,
pluginOptions: options,
PLUGIN_IGNORE_RESULT,
}),
},
......@@ -83,6 +89,19 @@ const pluginLoader = (config: Config, logger?: Logger): PluginManager => {
};
return {
urlTransform: async (url, req) => {
const fns = getFnsFromPlugins('urlTransform');
if (fns.length === 0) return url;
try {
const result = await tryEach(fns, {
onError: onPluginError,
ignoreResult: (r) => r === PLUGIN_IGNORE_RESULT,
})(url, req);
return result || url;
} catch (e) {
return url;
}
},
inputImageLoader: async (imageUrl) => {
const fns = getFnsFromPlugins('inputImageLoader');
try {
......@@ -93,7 +112,7 @@ const pluginLoader = (config: Config, logger?: Logger): PluginManager => {
return result;
} catch (e) {
throw new Error(
'[PLUGIN:inputImageLoader] failed. No plugin is able to handle this operation'
`[PLUGIN:inputImageLoader] failed. No plugin is able to download "${imageUrl}"`
);
}
},
......
import { Context } from '..';
import cache from '../caches/cache';
import { CacheConfig } from '../config/default';
import { ServerRequest } from 'microrouter';
const defaultInputCache: CacheConfig = {
type: 'none',
......@@ -12,14 +13,14 @@ interface ImageLoader {
* Given an url, returns the loaded image or raises an error
* @throws Error if cannot load the image
*/
get: (url: string) => Promise<Buffer>;
get: (url: string, req: ServerRequest) => Promise<Buffer>;
}
const imageLoader = (context: Context): ImageLoader => {
const { logger } = context;
const inputCache = cache(context.config.caches?.input || defaultInputCache);
return {
get: async (url) => {
get: async (url, req) => {
const cached = await inputCache.get(url);
if (cached) {
logger.debug(
......@@ -27,8 +28,9 @@ const imageLoader = (context: Context): ImageLoader => {
);
return cached.data;
}
const finalUrl = await context.pluginManager.urlTransform(url, req);
const loaded = (await context.pluginManager.inputImageLoader(
url
finalUrl
)) as Buffer | null;
if (loaded === null) {
throw new Error(`Cannot load image "${url}"`);
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment