Skip to content

Commit

Permalink
feat: Allow multiple context initializers (#12)
Browse files Browse the repository at this point in the history
  • Loading branch information
bertho-zero committed Jan 29, 2020
1 parent 4e24499 commit a556272
Show file tree
Hide file tree
Showing 6 changed files with 231 additions and 50 deletions.
98 changes: 77 additions & 21 deletions packages/hooks/src/base.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import { Middleware } from './compose';

export const HOOKS: string = Symbol('@feathersjs/hooks') as any;
export const CONTEXT: string = Symbol('@feathersjs/hooks/context') as any;

function walkOriginal (fn: any, method: any, res: any[] = []): any {
return typeof fn.original === 'function'
? walkOriginal(fn.original, method, [...res, ...method(fn)])
: [...res, ...method(fn)];
}

/**
*
* @param target The target object or function
* @param middleware
*/
Expand All @@ -19,6 +25,22 @@ export function getMiddleware<T> (target: any): Array<Middleware<T>> {
return (target && target[HOOKS]) || [];
}

/**
* @param target The target object or function
* @param updaters
*/
export function registerContextUpdater<T> (target: T, updaters: ContextUpdater[]) {
const current: ContextUpdater[] = (target as any)[CONTEXT] || [];

(target as any)[CONTEXT] = current.concat(updaters);

return target;
}

export function getContextUpdater<T> (target: any): Array<ContextUpdater<T>> {
return (target && target[CONTEXT]) || [];
}

/**
* The base hook context.
*/
Expand All @@ -38,7 +60,7 @@ export class HookContext<T = any, C = any> {
* A function that updates the hook context with the `this` reference and
* arguments of the function call.
*/
export type ContextUpdater<T = any> = (self: any, args: any[], context: HookContext<T>) => HookContext<T>;
export type ContextUpdater<T = any> = (self: any, fn: any, args: any[], context: HookContext<T>) => HookContext<T>;
/**
* A function that for a given function, calling context and arguments returns the list of hooks
*/
Expand All @@ -49,28 +71,39 @@ export type MiddlewareCollector<T = any> = (self: any, fn: any, args: any[]) =>
*/
export interface FunctionHookOptions<T = any> {
middleware: Array<Middleware<T>>;
context: ContextUpdater<T>;
context: Array<ContextUpdater<T>>;
collect: MiddlewareCollector<T>;
}

export type HookSettings<T = any> = Array<Middleware<T>>|Partial<FunctionHookOptions>;
export type HookSettings<T = any> = Array<Middleware<T>>|Partial<Omit<FunctionHookOptions, 'context'> & {
context: ContextUpdater<T>|Array<ContextUpdater<T>>;
}>;

export function defaultCollectMiddleware<T = any> (self: any, fn: any, _args: any[]) {
return [
...getMiddleware<T>(self),
...getMiddleware(fn)
...walkOriginal(fn, getMiddleware)
];
}

export function normalizeOptions<T = any> (opts: HookSettings): FunctionHookOptions<T> {
export function normalizeOptions<T = any> (opts: any): FunctionHookOptions<T> {
const options: Partial<FunctionHookOptions> = Array.isArray(opts) ? { middleware: opts } : opts;
const {
middleware = [],
context = withParams(),
collect = defaultCollectMiddleware
} = options;

return { middleware, context, collect };
const contextUpdaters = Array.isArray(context) ? context : [context];

return { middleware, context: contextUpdaters, collect };
}

export function collectContextUpdaters<T = any> (self: any, fn: any, _args: any[]) {
return [
...getContextUpdater<T>(self),
...walkOriginal(fn, getContextUpdater)
];
}

/**
Expand All @@ -80,22 +113,32 @@ export function normalizeOptions<T = any> (opts: HookSettings): FunctionHookOpti
*
* @param params The list of parameter names
*/
export function withParams<T = any> (...params: string[]) {
return (self: any, args: any[], context: HookContext<T>) => {
params.forEach((name, index) => {
context[name] = args[index];
export function withParams<T = any> (...params: Array<string | [string, any]>) {
return (self: any, _fn: any, args: any[], context: HookContext<T>) => {
params.forEach((param: string | [string, any], index: number) => {
if (typeof param === 'string') {
context[param] = args[index];
return;
}
const [name, defaultValue] = param;
context[name] = args[index] === undefined ? defaultValue : args[index];
});

if (params.length > 0) {
Object.defineProperty(context, 'arguments', {
get (this: HookContext<T>) {
const result = params.map(name => this[name]);

return Object.freeze(result);
}
});
} else {
context.arguments = args;
if (!context.arguments) {
if (params.length > 0) {
Object.defineProperty(context, 'arguments', {
get (this: HookContext<T>) {
const result = params.map(param => {
const name = typeof param === 'string' ? param : param[0];
return this[name];
});

return Object.freeze(result);
}
});
} else {
context.arguments = args;
}
}

if (self) {
Expand All @@ -105,3 +148,16 @@ export function withParams<T = any> (...params: string[]) {
return context;
};
}

/**
* Returns a ContextUpdater function that adds props on the hook context
*
* @param props The props object to assign
*/
export function withProps<T = any> (props: any) {
return (_self: any, _fn: any, _args: any[], context: HookContext<T>) => {
Object.assign(context, props);

return context;
};
}
10 changes: 3 additions & 7 deletions packages/hooks/src/decorator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { HookContext, registerMiddleware, normalizeOptions, HookSettings } from

export const hookDecorator = <T> (hooks: HookSettings<T> = []) => {
return (_target: any, method: string, descriptor: TypedPropertyDescriptor<any>): TypedPropertyDescriptor<any> => {
const options = normalizeOptions(hooks);
const { context, ...options } = normalizeOptions(hooks);

if (!descriptor) {
registerMiddleware(_target.prototype, options.middleware);
Expand All @@ -17,14 +17,10 @@ export const hookDecorator = <T> (hooks: HookSettings<T> = []) => {
throw new Error(`Can not apply hooks. '${method}' is not a function`);
}

const originalContext = options.context;
const context = (self: any, args: any[], context: HookContext<any>) => {
const ctx = originalContext(self, args, context);

context.push((_self: any, _fn: any, _args: any[], ctx: HookContext) => {
ctx.method = method;

return ctx;
};
});

descriptor.value = functionHooks(fn, {
...options,
Expand Down
31 changes: 25 additions & 6 deletions packages/hooks/src/function.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
import { compose, Middleware } from './compose';
import { HookContext, registerMiddleware, normalizeOptions, HookSettings } from './base';
import {
HookContext,
registerMiddleware,
registerContextUpdater,
normalizeOptions,
collectContextUpdaters,
HookSettings
} from './base';

function getOriginal (fn: any): any {
return typeof fn.original === 'function' ? getOriginal(fn.original) : fn;
}

/**
* Returns a new function that is wrapped in the given hooks.
Expand All @@ -9,21 +20,28 @@ import { HookContext, registerMiddleware, normalizeOptions, HookSettings } from
* and `context.self` to the function call `this` reference.
*
* @param original The function to wrap
* @param options A list of hooks (middleware) or options for more detailed hook processing
* @param opts A list of hooks (middleware) or options for more detailed hook processing
*/
export const functionHooks = <F, T = any>(original: F, opts: HookSettings<T>) => {
if (typeof original !== 'function') {
throw new Error('Can not apply hooks to non-function');
}

const { context: updateContext, collect, middleware } = normalizeOptions(opts);
const wrapper = function (this: any, ...args: any[]) {

const wrapper: any = function (this: any, ...args: any[]) {
// If we got passed an existing HookContext instance, we want to return it as well
const returnContext = args[args.length - 1] instanceof HookContext;
// Initialize the context. Either the default context or the one that was passed
const baseContext: HookContext = returnContext ? args.pop() : new HookContext();
let context: HookContext = returnContext ? args.pop() : new HookContext();

const contextUpdaters = collectContextUpdaters(this, wrapper, args);
// Initialize the context with the self reference and arguments
const context = updateContext(this, args, baseContext);

for (const contextUpdater of contextUpdaters) {
context = contextUpdater(this, wrapper, args, context);
}

// Assemble the hook chain
const hookChain: Middleware[] = [
// Return `ctx.result` or the context
Expand All @@ -33,7 +51,7 @@ export const functionHooks = <F, T = any>(original: F, opts: HookSettings<T>) =>
// Runs the actual original method if `ctx.result` is not already set
(ctx, next) => {
if (ctx.result === undefined) {
return Promise.resolve(original.apply(this, ctx.arguments)).then(result => {
return Promise.resolve(getOriginal(original).apply(this, ctx.arguments)).then(result => {
ctx.result = result;

return next();
Expand All @@ -47,6 +65,7 @@ export const functionHooks = <F, T = any>(original: F, opts: HookSettings<T>) =>
return compose(hookChain).call(this, context);
};

registerContextUpdater(wrapper, updateContext);
registerMiddleware(wrapper, middleware);

return Object.assign(wrapper, { original });
Expand Down
19 changes: 7 additions & 12 deletions packages/hooks/src/object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,27 +15,22 @@ export const objectHooks = (_obj: any, hooks: HookMap|Middleware[]) => {

return Object.keys(hooks).reduce((result, method) => {
const value = obj[method];
const options = normalizeOptions(hooks[method]);
const originalContext = options.context;
const context = (self: any, args: any[], context: HookContext<any>) => {
const ctx = originalContext(self, args, context);

ctx.method = method;

return ctx;
};
const { context, ...options } = normalizeOptions(hooks[method]);

if (typeof value !== 'function') {
throw new Error(`Can not apply hooks. '${method}' is not a function`);
}

const fn = functionHooks(value, {
context.push((_self: any, _fn: any, _args: any[], ctx: HookContext) => {
ctx.method = method;
return ctx;
});

result[method] = functionHooks(value, {
...options,
context
});

result[method] = fn;

return result;
}, obj);
};
66 changes: 63 additions & 3 deletions packages/hooks/test/function.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { strict as assert } from 'assert';
import {
hooks, HookContext, functionHooks,
NextFunction, getMiddleware, withParams, registerMiddleware
NextFunction, getMiddleware, registerMiddleware,
withParams, withProps
} from '../src/';

describe('functionHooks', () => {
const hello = async (name: string) => {
const hello = async (name: string, _params: any = {}) => {
return `Hello ${name}`;
};

Expand Down Expand Up @@ -168,6 +169,42 @@ describe('functionHooks', () => {
assert.equal(await fn('Dave'), 'Hello Changed');
});

it('creates context with default params', async () => {
const fn = hooks(hello, {
middleware: [
async (ctx, next) => {
assert.equal(ctx.name, 'Dave');
assert.deepEqual(ctx.params, {});

ctx.name = 'Changed';

await next();
}
],
context: withParams('name', ['params', {}])
});

assert.equal(await fn('Dave'), 'Hello Changed');
});

it('assigns props to context', async () => {
const fn = hooks(hello, {
middleware: [
async (ctx, next) => {
assert.equal(ctx.name, 'Dave');
assert.equal(ctx.dev, true);

ctx.name = 'Changed';

await next();
}
],
context: [withParams('name'), withProps({ dev: true })]
});

assert.equal(await fn('Dave'), 'Hello Changed');
});

it('with named context ctx.arguments is frozen', async () => {
const modifyArgs = async (ctx: HookContext, next: NextFunction) => {
ctx.arguments[0] = 'Test';
Expand Down Expand Up @@ -201,7 +238,7 @@ describe('functionHooks', () => {
});

const customContext = new HookContext({ message });
const resultContext: HookContext = await fn('Dave', customContext);
const resultContext: HookContext = await fn('Dave', {}, customContext);

assert.equal(resultContext, customContext);
assert.deepEqual(resultContext, new HookContext({
Expand All @@ -210,4 +247,27 @@ describe('functionHooks', () => {
result: 'Hello Changed'
}));
});

it('calls middleware one time', async () => {
let called = 0;

const sayHi = hooks((name: any) => `Hi ${name}`, [
async (_context, next) => {
called++;
await next();
}
]);

const exclamation = hooks(sayHi, [
async (context, next) => {
await next();
context.result += '!';
}
]);

const result = await exclamation('Bertho');

assert.equal(result, 'Hi Bertho!');
assert.equal(called, 1);
});
});
Loading

0 comments on commit a556272

Please sign in to comment.