Skip to content

Commit

Permalink
feat: custom depthLimit (circular references) (#29)
Browse files Browse the repository at this point in the history
  • Loading branch information
kamilkisiela committed Apr 19, 2019
1 parent 159aabf commit 8f4c83e
Show file tree
Hide file tree
Showing 5 changed files with 110 additions and 11 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

### vNEXT

- feat: custom `depthLimit` (circular references) [PR #29](https://github.com/Urigo/SOFA/pull/29)

### v0.2.3

- fix: prevent circular references [PR #23](https://github.com/Urigo/SOFA/pull/23)
Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,20 @@ Whenever Sofa tries to resolve an author of a message, instead of exposing an ID

> Pattern is easy: `Type:field` or `Type`
### Custom depth limit

Sofa prevents circular references by default, but only one level deep. In order to change it, set the `depthLimit` option to any number:

```ts
api.use(
'/api',
sofa({
schema,
depthLimit: 2,
})
);
```

### Custom execute phase

By default, Sofa uses `graphql` function from `graphql-js` to turn an operation into data but it's very straightforward to pass your own logic. Thanks to that you can even use a remote GraphQL Server (with Fetch or through Apollo Links).
Expand Down
53 changes: 43 additions & 10 deletions src/operation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,14 @@ export function buildOperation({
field,
models,
ignore,
depthLimit,
}: {
schema: GraphQLSchema;
kind: OperationTypeNode;
field: string;
models: string[];
ignore: Ignore;
depthLimit?: number;
}) {
resetOperationVariables();

Expand All @@ -70,6 +72,7 @@ export function buildOperation({
kind,
models,
ignore,
depthLimit: depthLimit || 1,
});

// attach variables
Expand All @@ -88,12 +91,14 @@ function buildDocumentNode({
kind,
models,
ignore,
depthLimit,
}: {
schema: GraphQLSchema;
fieldName: string;
kind: OperationTypeNode;
models: string[];
ignore: Ignore;
depthLimit: number;
}) {
const typeMap: Record<OperationTypeNode, GraphQLObjectType> = {
query: schema.getQueryType()!,
Expand Down Expand Up @@ -129,6 +134,7 @@ function buildDocumentNode({
path: [],
ancestors: [],
ignore,
depthLimit,
}),
],
},
Expand All @@ -149,6 +155,7 @@ function resolveSelectionSet({
path,
ancestors,
ignore,
depthLimit,
}: {
parent: GraphQLNamedType;
type: GraphQLNamedType;
Expand All @@ -157,14 +164,20 @@ function resolveSelectionSet({
ancestors: GraphQLNamedType[];
firstCall?: boolean;
ignore: Ignore;
depthLimit: number;
}): SelectionSetNode | undefined {
if (isUnionType(type)) {
const types = type.getTypes();

return {
kind: 'SelectionSet',
selections: types
.filter(t => !hasCircularRef([...ancestors, t]))
.filter(
t =>
!hasCircularRef([...ancestors, t], {
depth: depthLimit,
})
)
.map<InlineFragmentNode>(t => {
const fields = t.getFields();

Expand All @@ -187,6 +200,7 @@ function resolveSelectionSet({
path: [...path, fieldName],
ancestors,
ignore,
depthLimit,
});
}),
},
Expand Down Expand Up @@ -221,13 +235,14 @@ function resolveSelectionSet({
return {
kind: 'SelectionSet',
selections: Object.keys(fields)
.filter(
fieldName =>
!hasCircularRef([
...ancestors,
getNamedType(fields[fieldName].type),
])
)
.filter(fieldName => {
return !hasCircularRef(
[...ancestors, getNamedType(fields[fieldName].type)],
{
depth: depthLimit,
}
);
})
.map(fieldName => {
return resolveField({
type: type,
Expand All @@ -236,6 +251,7 @@ function resolveSelectionSet({
path: [...path, fieldName],
ancestors,
ignore,
depthLimit,
});
}),
};
Expand Down Expand Up @@ -298,6 +314,7 @@ function resolveField({
path,
ancestors,
ignore,
depthLimit,
}: {
type: GraphQLObjectType;
field: GraphQLField<any, any>;
Expand All @@ -306,6 +323,7 @@ function resolveField({
ancestors: GraphQLNamedType[];
firstCall?: boolean;
ignore: Ignore;
depthLimit: number;
}): SelectionNode {
const namedType = getNamedType(field.type);
let args: ArgumentNode[] = [];
Expand Down Expand Up @@ -350,6 +368,7 @@ function resolveField({
path: [...path, field.name],
ancestors: [...ancestors, type],
ignore,
depthLimit,
}),
arguments: args,
};
Expand All @@ -365,6 +384,20 @@ function resolveField({
};
}

function hasCircularRef(types: GraphQLNamedType[]): boolean {
return types.some((t, i) => types.indexOf(t) !== i);
function hasCircularRef(
types: GraphQLNamedType[],
config: {
depth: number;
} = {
depth: 1,
}
): boolean {
const type = types[types.length - 1];

if (isScalarType(type)) {
return false;
}

const size = types.filter(t => t.name === type.name).length;
return size > config.depth;
}
1 change: 1 addition & 0 deletions src/sofa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export interface SofaConfig {
execute?: ExecuteFn;
ignore?: Ignore; // treat an Object with an ID as not a model - accepts ['User', 'Message.author']
onRoute?: OnRoute;
depthLimit?: number;
}

export interface Sofa {
Expand Down
51 changes: 50 additions & 1 deletion tests/operation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ test('should work with Subscription', async () => {
);
});

test('should work with circular ref', async () => {
test('should work with circular ref (default depth limit === 1)', async () => {
const document = buildOperation({
schema: buildSchema(`
type A {
Expand Down Expand Up @@ -314,3 +314,52 @@ test('should work with circular ref', async () => {
`)
);
});

test('should work with circular ref (custom depth limit)', async () => {
const document = buildOperation({
schema: buildSchema(`
type A {
b: B
}
type B {
c: C
}
type C {
end: String
a: A
}
type Query {
a: A
}
`),
kind: 'query',
field: 'a',
models,
ignore: [],
depthLimit: 2,
})!;

expect(clean(document)).toEqual(
clean(`
query aQuery {
a {
b {
c {
end
a {
b {
c {
end
}
}
}
}
}
}
}
`)
);
});

0 comments on commit 8f4c83e

Please sign in to comment.