Skip to content

Commit

Permalink
Merge pull request #233 from apollostack/query-transform
Browse files Browse the repository at this point in the history
Pluggable query transforms
  • Loading branch information
Sashko Stubailo committed May 23, 2016
2 parents fe9535a + ce0121c commit e8d43a6
Show file tree
Hide file tree
Showing 7 changed files with 277 additions and 10 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ Expect active development and potentially significant breaking changes in the `0

### vNEXT

- Add support for basic query transformation before submitting to the server by passing an option to `ApolloClient` constructor. (e.g. adding `__typename` to each SelectionSet) [Issue #230](https://github.com/apollostack/apollo-client/issues/230) [PR #233](https://github.com/apollostack/apollo-client/pull/233)

### v0.3.10

- Resolve a race condition between `QueryManager` `stopQuery()` and `broadcastQueries()`, which would result in an error `listener is not a function`. [Issue #231](https://github.com/apollostack/apollo-client/issues/231) [PR #232](https://github.com/apollostack/apollo-client/pull/232)
Expand Down
24 changes: 20 additions & 4 deletions src/QueryManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ import {
getQueryDefinition,
} from './queries/getFromAST';

import {
QueryTransformer,
applyTransformerToOperation,
} from './queries/queryTransform';

import {
GraphQLResult,
Document,
Expand Down Expand Up @@ -88,7 +93,7 @@ export class QueryManager {
private store: ApolloStore;
private reduxRootKey: string;
private pollingTimer: NodeJS.Timer | any; // oddity in typescript

private queryTransformer: QueryTransformer;
private queryListeners: { [queryId: string]: QueryListener };

private idCounter = 0;
Expand All @@ -97,16 +102,19 @@ export class QueryManager {
networkInterface,
store,
reduxRootKey,
queryTransformer,
}: {
networkInterface: NetworkInterface,
store: ApolloStore,
reduxRootKey: string,
queryTransformer?: QueryTransformer,
}) {
// XXX this might be the place to do introspection for inserting the `id` into the query? or
// is that the network interface?
this.networkInterface = networkInterface;
this.store = store;
this.reduxRootKey = reduxRootKey;
this.queryTransformer = queryTransformer;

this.queryListeners = {};

Expand All @@ -133,8 +141,11 @@ export class QueryManager {
}): Promise<GraphQLResult> {
const mutationId = this.generateQueryId();

const mutationDef = getMutationDefinition(mutation);
const mutationString = print(mutation);
let mutationDef = getMutationDefinition(mutation);
if (this.queryTransformer) {
mutationDef = applyTransformerToOperation(mutationDef, this.queryTransformer);
}
const mutationString = print(mutationDef);

const request = {
query: mutationString,
Expand Down Expand Up @@ -162,6 +173,7 @@ export class QueryManager {
});

return result;

});
}

Expand Down Expand Up @@ -257,7 +269,11 @@ export class QueryManager {
returnPartialData = false,
} = options;

const queryDef = getQueryDefinition(query);
let queryDef = getQueryDefinition(query);
// Apply the query transformer if one has been provided.
if (this.queryTransformer) {
queryDef = applyTransformerToOperation(queryDef, this.queryTransformer);
}
const queryString = print(query);

// Parse the query passed in -- this could also be done by a build plugin or tagged
Expand Down
9 changes: 9 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ import {
IdGetter,
} from './data/extensions';

import {
QueryTransformer,
} from './queries/queryTransform';

import isUndefined = require('lodash.isundefined');

export {
Expand All @@ -47,22 +51,26 @@ export default class ApolloClient {
public initialState: any;
public queryManager: QueryManager;
public reducerConfig: ApolloReducerConfig;
public queryTransformer: QueryTransformer;

constructor({
networkInterface,
reduxRootKey,
initialState,
dataIdFromObject,
queryTransformer,
}: {
networkInterface?: NetworkInterface,
reduxRootKey?: string,
initialState?: any,
dataIdFromObject?: IdGetter,
queryTransformer?: QueryTransformer,
} = {}) {
this.reduxRootKey = reduxRootKey ? reduxRootKey : 'apollo';
this.initialState = initialState ? initialState : {};
this.networkInterface = networkInterface ? networkInterface :
createNetworkInterface('/graphql');
this.queryTransformer = queryTransformer;

this.reducerConfig = {
dataIdFromObject,
Expand Down Expand Up @@ -131,6 +139,7 @@ export default class ApolloClient {
networkInterface: this.networkInterface,
reduxRootKey: this.reduxRootKey,
store,
queryTransformer: this.queryTransformer,
});
};
}
28 changes: 23 additions & 5 deletions src/queries/queryTransform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ import {

import cloneDeep = require('lodash.clonedeep');

// Adds typename fields to every node in the AST recursively. Returns a copy of the entire
// AST with the typename fields added.
// Note: This muates the AST passed in.
export function addTypenameToSelectionSet(queryPiece: SelectionSet) {
// A QueryTransformer takes a SelectionSet and transforms it in someway (in place).
export type QueryTransformer = (queryPiece: SelectionSet) => void

// Adds a field with a given name to every node in the AST recursively.
// Note: this mutates the AST passed in.
export function addFieldToSelectionSet(fieldName: string, queryPiece: SelectionSet) {
if (queryPiece == null || queryPiece.selections == null) {
return queryPiece;
}
Expand All @@ -20,7 +22,7 @@ export function addTypenameToSelectionSet(queryPiece: SelectionSet) {
alias: null,
name: {
kind: 'Name',
value: '__typename',
value: fieldName,
},
};

Expand All @@ -37,10 +39,26 @@ export function addTypenameToSelectionSet(queryPiece: SelectionSet) {
return queryPiece;
}

// Adds typename fields to every node in the AST recursively.
// Note: This muates the AST passed in.
export function addTypenameToSelectionSet(queryPiece: SelectionSet) {
return addFieldToSelectionSet('__typename', queryPiece);
}

// Add typename field to the root query node (i.e. OperationDefinition). Returns a new
// query tree.
export function addTypenameToQuery(queryDef: OperationDefinition): OperationDefinition {
const queryClone = cloneDeep(queryDef);
this.addTypenameToSelectionSet(queryClone.selectionSet);
return queryClone;
}

// Apply a QueryTranformer to an OperationDefinition (extracted from a query
// or a mutation.)
// Returns a new query tree.
export function applyTransformerToOperation(queryDef: OperationDefinition,
queryTransformer: QueryTransformer): OperationDefinition {
const queryClone = cloneDeep(queryDef);
queryTransformer(queryClone.selectionSet);
return queryClone;
}
117 changes: 117 additions & 0 deletions test/QueryManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ import {
getIdField,
} from '../src/data/extensions';

import {
addTypenameToSelectionSet,
} from '../src/queries/queryTransform';

import gql from '../src/gql';

import {
Expand Down Expand Up @@ -1686,6 +1690,119 @@ describe('QueryManager', () => {
});
}, /wrap the query string in a "gql" tag/);
});

it('should transform queries correctly when given a QueryTransformer', (done) => {
const query = gql`
query {
author {
firstName
lastName
}
}`;
const transformedQuery = gql`
query {
author {
firstName
lastName
__typename
}
__typename
}`;
const unmodifiedQueryResult = {
'author': {
'firstName': 'John',
'lastName': 'Smith',
},
};
const transformedQueryResult = {
'author': {
'firstName': 'John',
'lastName': 'Smith',
'__typename': 'Author',
},
'__typename': 'RootQuery',
};

const networkInterface = mockNetworkInterface(
{
request: {query},
result: {data: unmodifiedQueryResult},
},
{
request: {query: transformedQuery},
result: {data: transformedQueryResult},
});

//make sure that the query is transformed within the query
//manager
const queryManagerWithTransformer = new QueryManager({
networkInterface: networkInterface,
store: createApolloStore(),
reduxRootKey: 'apollo',
queryTransformer: addTypenameToSelectionSet,
});


queryManagerWithTransformer.query({query: query}).then((result) => {
assert.deepEqual(result.data, transformedQueryResult);
done();
});
});

it('should transform mutations correctly', (done) => {
const mutation = gql`
mutation {
createAuthor(firstName: "John", lastName: "Smith") {
firstName
lastName
}
}`;
const transformedMutation = gql`
mutation {
createAuthor(firstName: "John", lastName: "Smith") {
firstName
lastName
__typename
}
__typename
}`;
const unmodifiedMutationResult = {
'createAuthor': {
'firstName': 'It works!',
'lastName': 'It works!',
},
};
const transformedMutationResult = {
'createAuthor': {
'firstName': 'It works!',
'lastName': 'It works!',
'__typename': 'Author',
},
'__typename': 'RootMutation',
};

const networkInterface = mockNetworkInterface(
{
request: {query: mutation},
result: {data: unmodifiedMutationResult},
},
{
request: {query: transformedMutation},
result: {data: transformedMutationResult},
});

const queryManagerWithTransformer = new QueryManager({
networkInterface: networkInterface,
store: createApolloStore(),
reduxRootKey: 'apollo',
queryTransformer: addTypenameToSelectionSet,
});

queryManagerWithTransformer.mutate({mutation: mutation}).then((result) => {
assert.deepEqual(result.data, transformedMutationResult);
done();
});
});
});

function testDiffing(
Expand Down
56 changes: 56 additions & 0 deletions test/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import {
HTTPNetworkInterface,
} from '../src/networkInterface';

import { addTypenameToSelectionSet } from '../src/queries/queryTransform';

import mockNetworkInterface from './mocks/mockNetworkInterface';

import * as chaiAsPromised from 'chai-as-promised';
Expand Down Expand Up @@ -463,6 +465,60 @@ describe('client', () => {
});
});

it('should be able to transform queries', (done) => {
const query = gql`
query {
author {
firstName
lastName
}
}`;
const transformedQuery = gql`
query {
author {
firstName
lastName
__typename
}
__typename
}`;

const result = {
'author': {
'firstName': 'John',
'lastName': 'Smith',
},
};
const transformedResult = {
'author': {
'firstName': 'John',
'lastName': 'Smith',
'__typename': 'Author',
},
'__typename': 'RootQuery',
};

const networkInterface = mockNetworkInterface(
{
request: { query },
result: { data: result },
},
{
request: { query: transformedQuery },
result: { data: transformedResult },
});

const client = new ApolloClient({
networkInterface,
queryTransformer: addTypenameToSelectionSet,
});

client.query({ query }).then((actualResult) => {
assert.deepEqual(actualResult.data, transformedResult);
done();
});
});

describe('accepts dataIdFromObject option', () => {
const query = gql`
query people {
Expand Down
Loading

0 comments on commit e8d43a6

Please sign in to comment.