Skip to content

Commit

Permalink
feat(Resolver): Add debug methods to resolver:
Browse files Browse the repository at this point in the history
debugParams() - console log resolveParams
debugPayload() - console log resolve payload
debugExecTime() - console log execution time
debug() - call debugParams, debugPayload and debugExecTime

Debug resolvers in runtime:
GQC.rootQuery().addFields({
  userById: UserTC.getResolver('findById').debugExecTime(),
  userByIds:
UserTC.getResolver('findByIds').debugParams().debugPayload(),
  userMany: UserTC.getResolver('findMany').debug(),
});

Also you may debug resolver structure in build phase:
console.log(UserTC.getResolver('findById').toString());
  • Loading branch information
nodkz committed Jun 24, 2017
1 parent f71f087 commit 8a9cd5c
Show file tree
Hide file tree
Showing 9 changed files with 735 additions and 7 deletions.
3 changes: 2 additions & 1 deletion .eslintrc
Expand Up @@ -26,7 +26,8 @@
"trailingComma": "es5",
}],
"import/prefer-default-export": 0,
"arrow-parens": 0
"arrow-parens": 0,
"no-use-before-define": ["error", { "functions": false }]
},
"env": {
"jasmine": true,
Expand Down
4 changes: 2 additions & 2 deletions package.json
Expand Up @@ -65,8 +65,8 @@
"watch": "jest --watch",
"coverage": "jest --coverage",
"lint": "eslint --ext .js ./src",
"flow": "./node_modules/.bin/flow stop && ./node_modules/.bin/flow",
"test": "npm run coverage && npm run lint && npm run flow",
"flow-reload": "./node_modules/.bin/flow stop && ./node_modules/.bin/flow",
"test": "npm run coverage && npm run lint && npm run flow-reload",
"link": "yarn link graphql && yarn link",
"unlink": "yarn unlink graphql && yarn add graphql",
"semantic-release": "semantic-release pre && npm publish && semantic-release post"
Expand Down
168 changes: 168 additions & 0 deletions src/__tests__/resolver-test.js
Expand Up @@ -677,4 +677,172 @@ describe('Resolver', () => {
expect(resolver.setKind('query')).toBe(resolver);
expect(resolver.setDescription('Find method')).toBe(resolver);
});

describe('debug methods', () => {
/* eslint-disable no-console */
const origConsole = global.console;
beforeEach(() => {
global.console = {
log: jest.fn(),
dir: jest.fn(),
time: jest.fn(),
timeEnd: jest.fn(),
};
});
afterEach(() => {
global.console = origConsole;
});

describe('debugExecTime()', () => {
it('should measure execution time', async () => {
const r1 = new Resolver({
name: 'find',
displayName: 'User.find()',
resolve: () => {},
});
await r1.debugExecTime().resolve();

expect(console.time.mock.calls[0]).toEqual(['Execution time for User.find()']);
expect(console.timeEnd.mock.calls[0]).toEqual(['Execution time for User.find()']);
});
});

describe('debugParams()', () => {
it('should show resolved payload', () => {
const r1 = new Resolver({
name: 'find',
displayName: 'User.find()',
resolve: () => {},
});
r1.debugParams().resolve({
source: { id: 1 },
args: { limit: 1 },
context: { isAdmin: true, db: {} },
info: { fieldName: 'a', otherAstFields: {} },
});

expect(console.log.mock.calls[0]).toEqual(['ResolveParams for User.find():']);
expect(console.dir.mock.calls[0]).toEqual([
{
args: { limit: 1 },
context: { db: 'Object {} [[hidden]]', isAdmin: true },
info: 'Object {} [[hidden]]',
source: { id: 1 },
'[debug note]':
'Some data was [[hidden]] to display this fields use debugParams("info context.db")',
},
{ colors: true, depth: 5 },
]);
});

it('should show filtered resolved payload', () => {
const r1 = new Resolver({
name: 'find',
displayName: 'User.find()',
resolve: () => {},
});
r1.debugParams('args, args.sort, source.name').resolve({
source: { id: 1, name: 'Pavel' },
args: { limit: 1, sort: 'id' },
});

expect(console.log.mock.calls[0]).toEqual(['ResolveParams for User.find():']);
expect(console.dir.mock.calls[0]).toEqual([
{
args: { limit: 1, sort: 'id' },
'args.sort': 'id',
'source.name': 'Pavel',
},
{ colors: true, depth: 5 },
]);
});
});

describe('debugPayload()', () => {
it('should show resolved payload', async () => {
const r1 = new Resolver({
name: 'find',
displayName: 'User.find()',
resolve: async () => ({ a: 123 }),
});
await r1.debugPayload().resolve();

expect(console.log.mock.calls[0]).toEqual(['Resolved Payload for User.find():']);
expect(console.dir.mock.calls[0]).toEqual([{ a: 123 }, { colors: true, depth: 5 }]);
});

it('should show filtered resolved payload', async () => {
const r1 = new Resolver({
name: 'find',
displayName: 'User.find()',
resolve: async () => ({ a: 123, b: 345, c: [0, 1, 2, 3] }),
});
await r1.debugPayload(['b', 'c.3']).resolve();

expect(console.log.mock.calls[0]).toEqual(['Resolved Payload for User.find():']);
expect(console.dir.mock.calls[0]).toEqual([
{ b: 345, 'c.3': 3 },
{ colors: true, depth: 5 },
]);
});

it('should show rejected payload', async () => {
const err = new Error('Request failed');
const r1 = new Resolver({
name: 'find',
displayName: 'User.find()',
resolve: async () => {
throw err;
},
});
await r1.debugPayload().resolve().catch(e => {});

expect(console.log.mock.calls[0]).toEqual(['Rejected Payload for User.find():']);
expect(console.log.mock.calls[1]).toEqual([err]);
});
});

describe('debug()', () => {
it('should output execution time, resolve params and payload', async () => {
const r1 = new Resolver({
name: 'find',
displayName: 'User.find()',
resolve: () => ({ a: 123, b: 345, c: [0, 1, 2, 3] }),
});

await r1
.debug({
params: 'args.sort source.name',
payload: 'b, c.3',
})
.resolve({
source: { id: 1, name: 'Pavel' },
args: { limit: 1, sort: 'id' },
});

expect(console.time.mock.calls[0]).toEqual(['Execution time for User.find()']);
expect(console.timeEnd.mock.calls[0]).toEqual(['Execution time for User.find()']);

expect(console.log.mock.calls[0]).toEqual([
'ResolveParams for debugExecTime(User.find()):',
]);
expect(console.dir.mock.calls[0]).toEqual([
{
'args.sort': 'id',
'source.name': 'Pavel',
},
{ colors: true, depth: 2 },
]);

expect(console.log.mock.calls[1]).toEqual([
'Resolved Payload for debugParams(debugExecTime(User.find())):',
]);
expect(console.dir.mock.calls[1]).toEqual([
{ b: 345, 'c.3': 3 },
{ colors: true, depth: 2 },
]);
});
});
/* eslint-enable no-console */
});
});
1 change: 1 addition & 0 deletions src/definition.js
Expand Up @@ -317,6 +317,7 @@ export type ResolverOpts<TSource, TContext> = {
resolve?: ResolverMWResolveFn<TSource, TContext>,
args?: ComposeFieldConfigArgumentMap,
name?: string,
displayName?: string,
kind?: ResolverKinds,
description?: string,
parent?: Resolver<TSource, TContext>,
Expand Down
1 change: 1 addition & 0 deletions src/index.js
Expand Up @@ -19,3 +19,4 @@ export * from './utils/misc';
export * from './utils/is';
export { default as toDottedObject } from './utils/toDottedObject';
export { default as deepmerge } from './utils/deepmerge';
export { default as filterByDotPaths } from './utils/filterByDotPaths';
135 changes: 131 additions & 4 deletions src/resolver.js
Expand Up @@ -18,6 +18,7 @@ import deepmerge from './utils/deepmerge';
import { resolveInputConfigsAsThunk } from './utils/configAsThunk';
import { only, clearName } from './utils/misc';
import { isFunction, isString } from './utils/is';
import filterByDotPaths from './utils/filterByDotPaths';
import { getProjectionFromAST } from './projection';
import type {
GraphQLArgumentConfig,
Expand All @@ -43,11 +44,18 @@ import type {
import InputTypeComposer from './inputTypeComposer';
import { typeByPath } from './typeByPath';

export type ResolveDebugOpts = {
showHidden?: boolean,
depth?: number,
colors?: boolean,
};

export default class Resolver<TSource, TContext> {
type: GraphQLOutputType;
args: GraphQLFieldConfigArgumentMap;
resolve: ResolverMWResolveFn<TSource, TContext>;
name: string;
displayName: ?string;
kind: ?ResolverKinds;
description: ?string;
parent: ?Resolver<TSource, TContext>;
Expand All @@ -57,6 +65,7 @@ export default class Resolver<TSource, TContext> {
throw new Error('For Resolver constructor the `opts.name` is required option.');
}
this.name = opts.name;
this.displayName = opts.displayName || null;
this.parent = opts.parent || null;
this.kind = opts.kind || null;
this.description = opts.description || '';
Expand Down Expand Up @@ -311,6 +320,7 @@ export default class Resolver<TSource, TContext> {
oldOpts[key] = this[key];
}
}
oldOpts.displayName = undefined;
oldOpts.args = { ...this.args };
return new Resolver({ ...oldOpts, ...opts });
}
Expand Down Expand Up @@ -521,17 +531,19 @@ export default class Resolver<TSource, TContext> {
}

getNestedName() {
const name = this.displayName || this.name;
if (this.parent) {
return `${this.name}(${this.parent.getNestedName()})`;
return `${name}(${this.parent.getNestedName()})`;
}
return this.name;
return name;
}

toString() {
toStringOld() {
function extendedInfo(resolver, spaces = '') {
return [
'Resolver(',
` name: ${resolver.name},`,
` displayName: ${resolver.displayName || ''},`,
` type: ${util.inspect(resolver.type, { depth: 2 })},`,
` args: ${util.inspect(resolver.args, { depth: 3 }).replace('\n', `\n ${spaces}`)},`,
` resolve: ${resolver.resolve
Expand All @@ -543,7 +555,122 @@ export default class Resolver<TSource, TContext> {
.filter(s => !!s)
.join(`\n ${spaces}`);
}

return extendedInfo(this);
}

toString(colors: boolean = true) {
return util.inspect(this.toDebugStructure(false), { depth: 20, colors }).replace(/\\n/g, '\n');
}

setDisplayName(name: string): this {
this.displayName = name;
return this;
}

toDebugStructure(colors: boolean = true): Object {
const info: any = {
name: this.name,
displayName: this.displayName,
type: util.inspect(this.type, { depth: 2, colors }),
args: this.args,
resolve: this.resolve ? this.resolve.toString() : this.resolve,
};
if (this.parent) {
info.resolve = [info.resolve, { 'Parent resolver': this.parent.toDebugStructure(colors) }];
}
return info;
}

debugExecTime(): Resolver<TSource, TContext> {
/* eslint-disable no-console */
return this.wrapResolve(
next => async rp => {
const name = `Execution time for ${this.getNestedName()}`;
console.time(name);
const res = await next(rp);
console.timeEnd(name);
return res;
},
'debugExecTime'
);
/* eslint-enable no-console */
}

debugParams(
filterPaths: ?(string | string[]),
opts?: ResolveDebugOpts = { colors: true, depth: 5 }
): Resolver<TSource, TContext> {
/* eslint-disable no-console */
return this.wrapResolve(
next => rp => {
console.log(`ResolveParams for ${this.getNestedName()}:`);
const data = filterByDotPaths(rp, filterPaths, {
// is hidden (use debugParams(["info"])) or debug({ params: ["info"]})
// `is hidden (use debugParams(["context.*"])) or debug({ params: ["context.*"]})`,
hideFields: rp && rp.context && rp.context.res && rp.context.params && rp.context.headers
? {
// looks like context is express request, colapse it
info: '[[hidden]]',
context: '[[hidden]]',
}
: {
info: '[[hidden]]',
'context.*': '[[hidden]]',
},
hideFieldsNote:
'Some data was [[hidden]] to display this fields use debugParams("%fieldNames%")',
});
console.dir(data, opts);
return next(rp);
},
'debugParams'
);
/* eslint-enable no-console */
}

debugPayload(
filterPaths: ?(string | string[]),
opts?: ResolveDebugOpts = { colors: true, depth: 5 }
): Resolver<TSource, TContext> {
/* eslint-disable no-console */
return this.wrapResolve(
next => async rp => {
try {
const res = await next(rp);
console.log(`Resolved Payload for ${this.getNestedName()}:`);
if (Array.isArray(res) && res.length > 3 && !filterPaths) {
console.dir(
[
filterPaths ? filterByDotPaths(res[0], filterPaths) : res[0],
`[debug note]: Other ${res.length - 1} records was [[hidden]]. ` +
'Use debugPayload("0 1 2 3 4") or debug({ payload: "0 1 2 3 4" }) for display this records',
],
opts
);
} else {
console.dir(filterPaths ? filterByDotPaths(res, filterPaths) : res, opts);
}
return res;
} catch (e) {
console.log(`Rejected Payload for ${this.getNestedName()}:`);
console.log(e);
throw e;
}
},
'debugPayload'
);
/* eslint-enable no-console */
}

debug(
filterDotPaths?: {
params?: ?(string | string[]),
payload?: ?(string | string[]),
},
opts?: ResolveDebugOpts = { colors: true, depth: 2 }
): Resolver<TSource, TContext> {
return this.debugExecTime()
.debugParams(filterDotPaths ? filterDotPaths.params : null, opts)
.debugPayload(filterDotPaths ? filterDotPaths.payload : null, opts);
}
}

0 comments on commit 8a9cd5c

Please sign in to comment.