Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
# Change Log
##### 1.5.1
- Bug fix, non-null validation should only apply on upserts or list items
---
### 1.5.0
- Add server-side validation for update args instead of preserving non-nullability from the origin type.
If a field is non-nullable it must be set in either the update operators (e.g. `setOnInsert`, `set`, `inc`, etc...)
Expand Down
66 changes: 26 additions & 40 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,29 @@ new GraphQLObjectType({
})
})
```
**We'll define the peron query in our GraphQL scheme like so:**
#### An example GraphQL query supported by the package:

Queries the first 50 persons, oldest first, over the age of 18, and whose first name is John

```
{
person (
filter: {
age: { GT: 18 },
name: {
firstName: { EQ: "John" }
}
},
sort: { age: DESC },
pagination: { limit: 50 }
) {
fullName
age
}
}
```

**To implement, we'll define the peron query field in our GraphQL scheme like so:**


```js
Expand All @@ -37,7 +59,7 @@ person: {
args: getGraphQLQueryArgs(PersonType),
resolve: getMongoDbQueryResolver(PersonType,
async (filter, projection, options, obj, args, context) => {
return await context.db.collection('persons').find(filter, projection, options).toArray();
return await context.db.collection('people').find(filter, projection, options).toArray();
})
}
```
Expand All @@ -55,7 +77,7 @@ You'll notice that integrating the package takes little more than adding some fa
* As of `mongodb` package version 3.0, you should implement the resolve callback as:
```js
options.projection = projection;
return await context.db.collection('persons').find(filter, options).toArray();
return await context.db.collection('people').find(filter, options).toArray();
```

### That's it!
Expand Down Expand Up @@ -109,41 +131,5 @@ age: SortType
limit: Int
skip: Int
```
#### Example GraphQL Query:

Queries the first 50 persons, oldest first, over the age of 18, and whose first name is John

```
{
person (
filter: {
age: { GT: 18 },
name: {
firstName: { EQ: "John" }
}
},
sort: { age: DESC },
pagination: { limit: 50 }
) {
fullName
age
}
}
```

### Aside from the mentioned above, the package comes with functionality galore!

* ```getGraphQLFilterType```
* ```getGraphQLSortType```
* ```getGraphQLUpdateType```
* ```getGraphQLInsertType```
* ```getGraphQLQueryArgs```
* ```getGraphQLUpdateArgs```
* ```GraphQLPaginationType```
* ```getMongoDbFilter```
* ```getMongoDbProjection```
* ```getMongoDbUpdate```
* ```getMongoDbSort```
* ```getMongoDbQueryResolver```
* ```getMongoDbUpdateResolver```
* ```setLogger```
### Functionality galore! Update, insert, and extensiable custom fields.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "graphql-to-mongodb",
"version": "1.5.0",
"version": "1.5.1",
"description": "Allows for generic run-time generation of filter types for existing graphql types and parsing client requests to mongodb find queries",
"main": "lib/index.js",
"types": "lib/index.d.ts",
Expand Down
15 changes: 8 additions & 7 deletions src/mongoDbUpdateValidation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,20 @@ export interface UpdateField {
export function validateUpdateArgs(updateArgs: UpdateArgs, graphQLType: GraphQLObjectType): void {
let errors: string[] = [];

errors = [...errors, ...validateNonNullableFields(Object.values(updateArgs), graphQLType)];
errors = errors.concat(validateNonNullableFields(
Object.keys(updateArgs).map(_ => updateArgs[_]), graphQLType, !!updateArgs.setOnInsert));

if (errors.length > 0) {
throw errors.join("\n");
}
}

export function validateNonNullableFields(objects: object[], graphQLType: GraphQLObjectType, path: string[] = []): string[] {
export function validateNonNullableFields(objects: object[], graphQLType: GraphQLObjectType, shouldAssert: boolean, path: string[] = []): string[] {
const typeFields = graphQLType.getFields();

const errors = validateNonNullableFieldsAssert(objects, typeFields, path);
const errors: string[] = shouldAssert ? validateNonNullableFieldsAssert(objects, typeFields, path) : [];

return [...errors, ...validateNonNullableFieldsTraverse(objects, typeFields, path)];
return [...errors, ...validateNonNullableFieldsTraverse(objects, typeFields, shouldAssert, path)];
}

export function validateNonNullableFieldsAssert(objects: object[], typeFields: GraphQLFieldMap<any, any>, path: string[] = []): string[] {
Expand Down Expand Up @@ -62,7 +63,7 @@ export function validateNonNullListField(fieldValues: object[], type: GraphQLTyp
return true;
}

export function validateNonNullableFieldsTraverse(objects: object[], typeFields: GraphQLFieldMap<any, any>, path: string[] = []): string[] {
export function validateNonNullableFieldsTraverse(objects: object[], typeFields: GraphQLFieldMap<any, any>, shouldAssert: boolean, path: string[] = []): string[] {
let keys: string[] = Array.from(new Set(flatten(objects.map(_ => Object.keys(_)))));

return keys.reduce((agg, key) => {
Expand All @@ -78,9 +79,9 @@ export function validateNonNullableFieldsTraverse(objects: object[], typeFields:
const values = objects.map(_ => _[key]).filter(_ => _);

if (isListType(type)) {
return [...agg, ...flatten(flattenListField(values, type).map(_ => validateNonNullableFields([_], innerType, newPath)))];
return [...agg, ...flatten(flattenListField(values, type).map(_ => validateNonNullableFields([_], innerType, true, newPath)))];
} else {
return [...agg, ...validateNonNullableFields(values, innerType, newPath)];
return [...agg, ...validateNonNullableFields(values, innerType, shouldAssert, newPath)];
}
}, []);
}
Expand Down
82 changes: 77 additions & 5 deletions tests/specs/mongoDbUpdateValidation.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,82 @@
import { ObjectType } from "../utils/types";
import { validateNonNullableFields, validateNonNullableFieldsAssert, validateNonNullListField, validateNonNullableFieldsTraverse, flattenListField } from "../../src/mongoDbUpdateValidation";
import { validateUpdateArgs, validateNonNullableFields, validateNonNullableFieldsAssert, validateNonNullListField, validateNonNullableFieldsTraverse, flattenListField } from "../../src/mongoDbUpdateValidation";
import { expect } from "chai";
import { GraphQLObjectType, GraphQLType, GraphQLList, GraphQLNonNull, GraphQLString, execute } from "graphql";
import { GraphQLObjectType, GraphQLType, GraphQLList, GraphQLNonNull, GraphQLString } from "graphql";
import { UpdateArgs } from "../../src/mongoDbUpdate";

describe("mongoDbProjection", () => {
describe("mongoDbUpdateValidation", () => {
describe("validateUpdateArgs", () => {
const tests: { description: string, type: GraphQLObjectType, updateArgs: UpdateArgs, expectedErrors: string[] }[] = [{
description: "Should invalidate non-null on upsert",
type: ObjectType,
updateArgs: {
setOnInsert: {
stringScalar: "x",
},
set: {
nonNullScalar: null
}
},
expectedErrors: ["Missing non-nullable field \"nonNullList\"", "Non-nullable field \"nonNullScalar\" is set to null"]
}, {
description: "Should ignore non-null on update",
type: ObjectType,
updateArgs: {
set: {
nonNullScalar: null
}
},
expectedErrors: []
}, {
description: "Should invalidate non-null on update list item",
type: ObjectType,
updateArgs: {
set: {
nonNullScalar: null,
nestedList: [{
}]
}
},
expectedErrors: ["Missing non-nullable field \"nestedList.nonNullList\"", "Missing non-nullable field \"nestedList.nonNullScalar\""]
}, {
description: "Should validate update correct non-null",
type: ObjectType,
updateArgs: {
setOnInsert: {
stringScalar: "x",
},
set: {
nonNullScalar: "x",
nonNullList: []
}
},
expectedErrors: []
}];

tests.forEach(test => it(test.description, () => {
// Arrange
let error;

// Act
try {
validateUpdateArgs(test.updateArgs, test.type);
} catch (err) {
error = err;
}

// Assert
if (test.expectedErrors.length > 0) {
expect(error, "error object expected").to.not.be.undefined;

const errorString: string = typeof error == "string" ? error : (error as Error).message;
const errors = errorString.split("\n");
expect(errors).to.have.members(test.expectedErrors, "Should detect correct errors");
} else {
if (error) throw error
}
}));
});

describe("validateNonNullableFields", () => {
const tests: { description: string, type: GraphQLObjectType, updateArgs: UpdateArgs, expectedErrors: string[] }[] = [{
description: "Should invalidate root fields",
Expand Down Expand Up @@ -91,7 +163,7 @@ describe("mongoDbProjection", () => {
const objects = Object.keys(test.updateArgs).map(_ => test.updateArgs[_]);

// Act
const errors = validateNonNullableFields(objects, test.type);
const errors = validateNonNullableFields(objects, test.type, true);

// Assert
expect(errors).to.have.members(test.expectedErrors, "Should detect correct errors");
Expand Down Expand Up @@ -217,7 +289,7 @@ describe("mongoDbProjection", () => {

tests.forEach(test => it(test.description, () => {
// act
const errors = validateNonNullableFieldsTraverse(test.objects, test.type.getFields(), test.path);
const errors = validateNonNullableFieldsTraverse(test.objects, test.type.getFields(), true, test.path);

// Assert
expect(errors).to.have.members(test.expectedErrors, "Should detect correct errors");
Expand Down
3 changes: 1 addition & 2 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@
"lib": [
"es7",
"dom",
"esnext.asynciterable",
"es2017.object"
"esnext.asynciterable"
],
"allowSyntheticDefaultImports": true,
"declaration": true,
Expand Down