Skip to content

Commit

Permalink
feat(data-layer): add graphql support
Browse files Browse the repository at this point in the history
Adds the ability to handle graphql Queries as Rxjs streams,
with the resolvers for those queries also being streams.
  • Loading branch information
Fabs committed May 11, 2018
1 parent 9380ac1 commit 36c4224
Show file tree
Hide file tree
Showing 3 changed files with 314 additions and 0 deletions.
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@
"dcos-dygraphs": "1.1.0-beta.3",
"deep-equal": "1.0.1",
"flux": "2.1.1",
"graphql": "0.13.2",
"graphql-tag": "2.8.0",
"graphql-tools": "2.23.1",
"intl": "1.2.5",
"inversify": "4.3.0",
"less-color-lighten": "0.0.1",
Expand All @@ -66,6 +69,7 @@
"react-transition-group": "1.2.1",
"reactjs-components": "0.20.3",
"reactjs-mixin": "0.0.2",
"recompose": "0.26.0",
"redux": "3.3.1",
"reflect-metadata": "0.1.12",
"rxjs": "5.4.3",
Expand Down
218 changes: 218 additions & 0 deletions packages/data-service/__tests__/graphqlObservable-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
import { Observable } from "rxjs/Observable";
import "rxjs/add/observable/interval";
import "rxjs/add/observable/of";
import "rxjs/add/operator/combineLatest";
import "rxjs/add/operator/map";
import "rxjs/add/operator/take";

import { marbles } from "rxjs-marbles/jest";

import { makeExecutableSchema } from "graphql-tools";
import gql from "graphql-tag";

import { graphqlObservable } from "../graphqlObservable";

const typeDefs = `
type Shuttle {
name: String!
}
type Query {
launched(name: String): [Shuttle!]!
}
`;

const resolvers = {
Query: {
launched: (parent, args, ctx) => {
const { name } = args;

// act according with the type of filter
if (name === undefined) {
return ctx.query;
} else if (typeof name == "string") {
return ctx.query.map(els => els.filter(el => el.name === name));
} else {
return ctx.query
.combineLatest(name, (res, name) => [res, name])
.map(els => els[0].filter(el => el.name === els[1]));
}
}
}
};

const schema = makeExecutableSchema({
typeDefs,
resolvers
});

// jest helper who binds the marbles for you
const itMarbles = (title, test) => {
return it(
title,
marbles(m => {
m.bind();
test(m);
})
);
};

describe("graphqlObservable", function() {
describe("Query", function() {
itMarbles("solves listing all fields", function(m) {
const query = gql`
query {
launched {
name
}
}
`;

const expectedData = [{ name: "discovery" }];
const dataSource = Observable.of(expectedData);
const expected = m.cold("(a|)", { a: { launched: expectedData } });

const result = graphqlObservable(query, schema, { query: dataSource });

m.expect(result.take(1)).toBeObservable(expected);
});

itMarbles("filters by variable argument", function(m) {
const query = gql`
query {
launched(name: $nameFilter) {
name
firstFlight
}
}
`;

const expectedData = [{ name: "discovery" }, { name: "challenger" }];
const dataSource = Observable.of(expectedData);
const expected = m.cold("(a|)", { a: { launched: [expectedData[0]] } });

const nameFilter = Observable.of("discovery");
const result = graphqlObservable(query, schema, {
query: dataSource,
nameFilter
});

m.expect(result.take(1)).toBeObservable(expected);
});

itMarbles("filters by static argument", function(m) {
const query = gql`
query {
launched(name: "discovery") {
name
firstFlight
}
}
`;

const expectedData = [{ name: "discovery" }, { name: "challenger" }];
const dataSource = Observable.of(expectedData);
const expected = m.cold("(a|)", { a: { launched: [expectedData[0]] } });

const result = graphqlObservable(query, schema, {
query: dataSource
});

m.expect(result.take(1)).toBeObservable(expected);
});

itMarbles("filters out fields", function(m) {
const query = gql`
query {
launched {
name
}
}
`;

const expectedData = [{ name: "discovery", firstFlight: 1984 }];
const dataSource = Observable.of(expectedData);
const expected = m.cold("(a|)", {
a: { launched: [{ name: "discovery" }] }
});

const result = graphqlObservable(query, schema, {
query: dataSource
});

m.expect(result.take(1)).toBeObservable(expected);
});

itMarbles("filters out fields", function(m) {
const query = gql`
query {
launched {
name
}
}
`;

const expectedData = [{ name: "discovery", firstFlight: 1984 }];
const dataSource = Observable.of(expectedData);
const expected = m.cold("(a|)", {
a: { launched: [{ name: "discovery" }] }
});

const result = graphqlObservable(query, schema, {
query: dataSource
});

m.expect(result.take(1)).toBeObservable(expected);
});

itMarbles("resolve with query alias", function(m) {
const query = gql`
query nasa {
launched {
name
}
}
`;

const expectedData = [{ name: "discovery", firstFlight: 1984 }];
const dataSource = Observable.of(expectedData);
const expected = m.cold("(a|)", {
a: { nasa: [{ name: "discovery" }] }
});

const result = graphqlObservable(query, schema, {
query: dataSource
});

m.expect(result.take(1)).toBeObservable(expected);
});

itMarbles("resolve multiple queries with alias", function(m) {
const query = gql`
query nasa {
launched {
name
}
}
query roscosmos {
launched {
name
}
}
`;

const expectedData = [{ name: "discovery", firstFlight: 1984 }];
const dataSource = Observable.of(expectedData);
const expected = m.cold("(a|)", {
a: { nasa: [{ name: "discovery" }], roscosmos: [{ name: "discovery" }] }
});

const result = graphqlObservable(query, schema, {
query: dataSource
});

m.expect(result.take(1)).toBeObservable(expected);
});
});
});
92 changes: 92 additions & 0 deletions packages/data-service/graphqlObservable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { Observable } from "rxjs/Observable";

// TODO: refactor this once it has a more compreehensive implementation
// of the graphql api

// WARNING: This is NOT a spec complete graphql implementation
// https://facebook.github.io/graphql/October2016/

/* eslint-disable-next-line import/prefer-default-export */
export const graphqlObservable = (doc, schema, context) => {
const translateOperation = {
query: "Query"
};

const resolveStep = (typeMap, definition, context, parent) => {
if (definition.kind === "OperationDefinition") {
const nextTypeMap = typeMap[
translateOperation[definition.operation]
].getFields();

return definition.selectionSet.selections.reduce((acc, sel) => {
const resolvedObservable = resolveStep(nextTypeMap, sel, context);

const merger = (acc, emitted) => {
const propertyName = (definition.name || sel.name).value;

return { ...acc, [propertyName]: emitted };
};

return acc.combineLatest(resolvedObservable, merger);
}, Observable.of({}));
}

// Node Field
if (definition.kind === "Field" && definition.selectionSet !== undefined) {
const args = definition.arguments
.map(arg => {
if (arg.value.kind === "Variable") {
return { [arg.name.value]: context[arg.value.name.value] };
} else {
return { [arg.name.value]: arg.value.value };
}
})
.reduce(Object.assign, {});

const resolvedObservable = typeMap[definition.name.value].resolve(
parent,
args,
context,
null // that would be the info
);

return resolvedObservable.map(emittedResults => {
return emittedResults.map(result => {
return definition.selectionSet.selections.reduce((acc, sel) => {
acc[sel.name.value] = resolveStep(typeMap, sel, context, result);

return acc;
}, {});
});
});
}

// LeafField
if (definition.kind === "Field") {
return parent[definition.name.value];
}

return Observable.throw(
new Error("graphqlObservable does not recognise ${definition.kind}")
);
};

if (doc.definitions.length === 1) {
return resolveStep(schema._typeMap, doc.definitions[0], context, null);
}

return doc.definitions.reduce((acc, definition) => {
const resolvedObservable = resolveStep(
schema._typeMap,
definition,
context,
null
);

const merger = (acc, resolved) => {
return { ...acc, ...resolved };
};

return acc.combineLatest(resolvedObservable, merger);
}, Observable.of({}));
};

0 comments on commit 36c4224

Please sign in to comment.