-
Notifications
You must be signed in to change notification settings - Fork 82
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(data-layer): add graphql support
Adds the ability to handle graphql Queries as Rxjs streams, with the resolvers for those queries also being streams.
- Loading branch information
Showing
3 changed files
with
314 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
218 changes: 218 additions & 0 deletions
218
packages/data-service/__tests__/graphqlObservable-test.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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({})); | ||
}; |