@dcos/data-service
The goal of this project is to make access to the data as simple as writing a GraphQL query
API
Mediator Pattern
The mediator pattern is used here
to decouple data handling from the view components by encapsulating the way they
communicate.
With this pattern, a component will receive down all the pieces of data (data down)
it needs via props,
and send events up
so the mediator can react to events. The key concept here is that both the
data provider and the view component have no idea how each other work.
The caller of componentFromStream
bellow is the mediator responsible for the exchange between
the part of the application which handles data, and the UI components.
componentFromStream(component$: Observable<React.Component>): React.Component;
Note that the way we achieve the Mediator pattern is not in an OO fashion, but
by abstracting all the communication between the data sources and the view components
by using a stream.
Query Example
This example uses a stream of repository of packages
that are fetched via one or more
APIs (abstracted here by a single stream). We use graphql to describe the query and streams
to achieve the concrete implementation. We also use streams to pass event from the components
in order to implement a search filter.
This example is an extract from DC/OS UI.
import React from "react";
import { Observable } from "rxjs";
import {
createEventHandler,
componentFromStream,
graphqlObservable
} from "@dcos/data-service";
import { makeExecutableSchema } from "graphql-tools";
import gql from "graphql-tag";
import { fetchRepositories } from "#PLUGINS/catalog/src/js/repositories/repositoriesStream";
export const typeDefs = `
type PackageRepository {
id: ID!
name: String!
uri: String!
}
type Query {
packageRepository(filter: String): [PackageRepository!]!
}
type Mutation {
addPackageRepository(uri: String!, name: String!): PackageRepository
}
`;
export const resolvers = {
Query: {
packageRepository: (parent, args, context) => {
const { filter } = args;
return context.query.combineLatest(filter, (result, filter) =>
Object.values(
new RepositoryList({ items: result.repositories })
.filterItemsByText(filter)
.getItems()
)
);
}
}
};
import RepositoriesTabUI from "#SRC/js/components/RepositoriesTabUI";
import RepositoryList from "#SRC/js/structs/RepositoryList";
const schema = makeExecutableSchema({
typeDefs,
resolvers
});
const packageRepositoryQuery = gql`
query {
packageRepository(filter: $filter) {
name
uri
}
}
`;
const { handler: onSearch, stream: searchEvent$ } = createEventHandler();
const searchTerm$ = searchEvent$.startWith("");
const components$ = graphqlObservable(packageRepositoryQuery, schema, {
query: fetchRepositories(),
filter: searchTerm$
})
.combineLatest(searchTerm$, (result, searchTerm) => {
return {
packageRepository: new RepositoryList({
items: result.packageRepository
}),
searchTerm
};
})
.map(data => {
return (
<RepositoriesTabUI
repositories={data.packageRepository}
searchTerm={data.searchTerm}
onSearch={onSearch}
/>
);
})
.startWith(<RepositoriesTabUI.Loading />)
.catch(err => Observable.of(<RepositoriesTabUI.Error err={err} />));
const RepositoriesTab = componentFromStream(() => components$);
RepositoriesTab.routeConfig = {
label: "Package Repositories",
matches: /^\/settings\/repositories/
};
module.exports = RepositoriesTab;
Notice how easy is to provide error handling whenever an exception happens and how
we could start the stream of components providing a loading component.
To pass information from the components to the graphql, we use another capability
from recompose
, the createEventHandler()
.
const { handler: onSearch, stream: searchEvent$ } = createEventHandler();