Skip to content

Commit

Permalink
fix(projection): improve nested merging and add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
nodkz committed Oct 20, 2017
1 parent 5c39f6e commit fde1513
Show file tree
Hide file tree
Showing 3 changed files with 250 additions and 18 deletions.
182 changes: 182 additions & 0 deletions src/__tests__/projection-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
/* @flow */

import { graphql } from '../graphql';
import type { GraphQLResolveInfo, GraphQLObjectType } from '../graphql';
import { getProjectionFromAST, extendByFieldProjection } from '../projection';
import TypeComposer from '../typeComposer';
import gqc from '../gqc';

const Level2TC = TypeComposer.create({
name: 'Level2',
fields: {
field2a: 'String',
field2b: 'Int',
withProjection2: {
type: 'Int',
projection: {
field2b: true,
},
},
},
});
const Level1TC = TypeComposer.create({
name: 'Level1',
fields: {
field1a: [Level2TC],
field1b: 'Int',
withProjection1: {
type: 'Int',
projection: {
field1b: true,
field1a: { field2a: true },
},
},
},
});
const resolve = jest.fn(() => ({}));
gqc.rootQuery().addFields({ field0: { type: Level1TC, resolve } });
const schema = gqc.buildSchema();

const getResolveInfo = async (query: string): Promise<GraphQLResolveInfo> => {
resolve.mockClear();
const res = await graphql(schema, query);
if (res && res.errors) {
throw new Error(res.errors[0]);
}
return resolve.mock.calls[0][3];
};

describe('projection', () => {
describe('getProjectionFromAST()', () => {
it('simple query', async () => {
const info = await getResolveInfo(`
query {
field0 {
field1a { field2a }
field1b
}
}
`);
expect(getProjectionFromAST(info)).toEqual({
field1a: { field2a: {} },
field1b: {},
});
});

it('inline fragments', async () => {
const info = await getResolveInfo(`
query {
field0 {
field1a { field2a }
... {
field1a { field2b }
field1b
}
}
}
`);
expect(getProjectionFromAST(info)).toEqual({
field1a: { field2a: {}, field2b: {} },
field1b: {},
});
});

it('fragment spreads', async () => {
const info = await getResolveInfo(`
query {
field0 {
...Frag
field1b
}
}
fragment Frag on Level1 {
field1a {
field2b
}
}
`);
expect(getProjectionFromAST(info)).toEqual({
field1a: { field2b: {} },
field1b: {},
});
});

it('fragment spreads with deep merge', async () => {
const info = await getResolveInfo(`
query {
field0 {
...Frag
field1a {
field2a
}
}
}
fragment Frag on Level1 {
field1a {
field2b
}
}
`);
expect(getProjectionFromAST(info)).toEqual({
field1a: { field2a: {}, field2b: {} },
});
});

it('extend by field.projection', async () => {
const info = await getResolveInfo(`
query {
field0 {
withProjection1
}
}
`);
expect(getProjectionFromAST(info)).toEqual({
withProjection1: {},
field1b: true,
field1a: { field2a: true },
});
});

it('extend by field.projection deep', async () => {
const info = await getResolveInfo(`
query {
field0 {
field1a {
withProjection2
}
}
}
`);
// console.dir(info, { colors: true, depth: 3 });
expect(getProjectionFromAST(info)).toEqual({
field1a: { withProjection2: {}, field2b: true },
});
});
});

describe('extendByFieldProjection()', () => {
it('first level', () => {
const type: GraphQLObjectType = (schema.getType('Level1'): any);
const projection = {
withProjection1: true,
};
expect(extendByFieldProjection(type, projection)).toEqual({
field1a: { field2a: true },
field1b: true,
withProjection1: true,
});
});

it('second level', () => {
const type: GraphQLObjectType = (schema.getType('Level1'): any);
const projection = {
field1a: { withProjection2: {} },
};
expect(extendByFieldProjection(type, projection)).toEqual({
field1a: { field2b: true, withProjection2: {} },
});
});
});
});
82 changes: 66 additions & 16 deletions src/projection.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@ import type {
FragmentDefinitionNode,
FragmentSpreadNode,
InlineFragmentNode,
} from 'graphql/language/ast';
import type { GraphQLResolveInfo, GraphQLOutputType } from 'graphql/type/definition';
import { FIELD, FRAGMENT_SPREAD, INLINE_FRAGMENT } from 'graphql/language/kinds';
GraphQLResolveInfo,
GraphQLOutputType,
} from './graphql';
import { Kind, GraphQLObjectType, GraphQLList, GraphQLNonNull } from './graphql';
import deepmerge from './utils/deepmerge';

const { FIELD, FRAGMENT_SPREAD, INLINE_FRAGMENT } = Kind;

// export type ProjectionType = { [fieldName: string]: $Shape<ProjectionNode> | true };
// export type ProjectionNode = { [fieldName: string]: $Shape<ProjectionNode> } | true;
Expand All @@ -24,6 +28,19 @@ export function getProjectionFromAST(
return {};
}

const queryProjection = getProjectionFromASTquery(context, fieldNode);
const queryExtProjection = extendByFieldProjection(context.returnType, queryProjection);
return queryExtProjection;
}

export function getProjectionFromASTquery(
context: GraphQLResolveInfo,
fieldNode?: FieldNode | InlineFragmentNode | FragmentDefinitionNode
): ProjectionType {
if (!context) {
return {};
}

let selections; // Array<FieldNode | InlineFragmentNode | FragmentSpreadNode>;
if (fieldNode) {
if (fieldNode.selectionSet) {
Expand All @@ -40,21 +57,24 @@ export function getProjectionFromAST(
}

const projection = (selections || []
).reduce((list, ast: FieldNode | InlineFragmentNode | FragmentSpreadNode) => {
).reduce((res, ast: FieldNode | InlineFragmentNode | FragmentSpreadNode) => {
switch (ast.kind) {
case FIELD:
list[ast.name.value] = getProjectionFromAST(context, ast) || true;
return list;
case FIELD: {
const { value } = ast.name;
if (res[value]) {
res[value] = deepmerge(res[value], getProjectionFromASTquery(context, ast) || true);
} else {
res[value] = getProjectionFromASTquery(context, ast) || true;
}
return res;
}
case INLINE_FRAGMENT:
return {
...list,
...getProjectionFromAST(context, ast),
};
return deepmerge(res, getProjectionFromASTquery(context, ast));
case FRAGMENT_SPREAD:
return {
...list,
...getProjectionFromAST(context, context.fragments[ast.name.value]),
};
return deepmerge(
res,
getProjectionFromASTquery(context, context.fragments[ast.name.value])
);
default:
throw new Error('Unsuported query selection');
}
Expand Down Expand Up @@ -86,10 +106,40 @@ export function getFlatProjectionFromAST(
context: GraphQLResolveInfo,
fieldNodes?: FieldNode | InlineFragmentNode | FragmentDefinitionNode
) {
const projection = getProjectionFromAST(context, fieldNodes) || {};
const projection = getProjectionFromASTquery(context, fieldNodes) || {};
const flatProjection = {};
Object.keys(projection).forEach(key => {
flatProjection[key] = !!projection[key];
});
return flatProjection;
}

// This method traverse fields and extends current projection
// by projection from fields
export function extendByFieldProjection(
returnType: GraphQLOutputType,
projection: ProjectionType
): ProjectionType {
let type: GraphQLOutputType = returnType;

while (type instanceof GraphQLList || type instanceof GraphQLNonNull) {
type = type.ofType;
}

if (!(type instanceof GraphQLObjectType)) {
return projection;
}

let proj = projection;
Object.keys(proj).forEach(key => {
// $FlowFixMe
const field = type._fields[key];
if (!field) return;

if (field.projection) proj = deepmerge(proj, field.projection);
// $FlowFixMe
proj[key] = extendByFieldProjection(field.type, proj[key]);
});

return proj;
}
4 changes: 2 additions & 2 deletions src/utils/deepmerge.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
/* eslint-disable prefer-template, no-param-reassign, no-lonely-if */
// https://github.com/KyleAMathews/deepmerge/blob/master/index.js

export default function deepmerge(target: any, src: any) {
export default function deepmerge<T: any>(target: any, src: T): T {
const array = Array.isArray(src);
let dst = (array && []) || {};
let dst: T = ((array && []) || {}: any);

if (array) {
target = target || [];
Expand Down

0 comments on commit fde1513

Please sign in to comment.