Skip to content

Commit b6e53af

Browse files
author
yoavkarako
authored
1.5.0 (OS-Guild#31)
1 parent 86f0df7 commit b6e53af

14 files changed

+432
-58
lines changed

CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
# Change Log
2+
### 1.5.0
3+
- Add server-side validation for update args instead of preserving non-nullability from the origin type.
4+
If a field is non-nullable it must be set in either the update operators (e.g. `setOnInsert`, `set`, `inc`, etc...)
5+
- Tests! (Limited coverage)
6+
---
27
##### 1.4.4
38
- Bug fix
49
---
@@ -30,7 +35,7 @@
3035
- Fix type declarations in package
3136
---
3237
### 1.3.0
33-
- Renameed package and repository
38+
- Renamed package and repository
3439
- TypeScript!
3540
- Log warn and error callbacks
3641
---

index.ts

Lines changed: 12 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,12 @@
1-
import { getGraphQLFilterType } from './src/graphQLFilterType';
2-
import getMongoDbFilter from './src/mongoDbFilter';
3-
import { getGraphQLUpdateType, getGraphQLInsertType } from './src/graphQLMutationType';
4-
import getMongoDbUpdate from './src/mongoDbUpdate';
5-
import GraphQLPaginationType from './src/graphQLPaginationType';
6-
import getGraphQLSortType from './src/graphQLSortType';
7-
import getMongoDbSort from './src/mongoDbSort';
8-
import { getMongoDbProjection } from './src/mongoDbProjection';
9-
import { getMongoDbQueryResolver, getGraphQLQueryArgs, QueryOptions } from './src/queryResolver';
10-
import { getMongoDbUpdateResolver, getGraphQLUpdateArgs, UpdateOptions } from './src/updateResolver';
11-
import { setLogger } from './src/logger';
12-
13-
export {
14-
getGraphQLFilterType,
15-
getMongoDbFilter,
16-
getGraphQLUpdateType,
17-
getGraphQLInsertType,
18-
getMongoDbUpdate,
19-
GraphQLPaginationType,
20-
getGraphQLSortType,
21-
getMongoDbSort,
22-
getMongoDbProjection,
23-
QueryOptions,
24-
getMongoDbQueryResolver,
25-
getGraphQLQueryArgs,
26-
UpdateOptions,
27-
getMongoDbUpdateResolver,
28-
getGraphQLUpdateArgs,
29-
setLogger
30-
};
1+
export { getGraphQLFilterType } from './src/graphQLFilterType';
2+
export { default as getMongoDbFilter } from './src/mongoDbFilter';
3+
export { getGraphQLUpdateType, getGraphQLInsertType } from './src/graphQLMutationType';
4+
export { default as getMongoDbUpdate } from './src/mongoDbUpdate';
5+
export { validateUpdateArgs } from "./src/mongoDbUpdateValidation";
6+
export { default as GraphQLPaginationType } from './src/graphQLPaginationType';
7+
export { default as getGraphQLSortType } from './src/graphQLSortType';
8+
export { default as getMongoDbSort } from './src/mongoDbSort';
9+
export { getMongoDbProjection } from './src/mongoDbProjection';
10+
export { getMongoDbQueryResolver, getGraphQLQueryArgs, QueryOptions } from './src/queryResolver';
11+
export { getMongoDbUpdateResolver, getGraphQLUpdateArgs, UpdateOptions } from './src/updateResolver';
12+
export { setLogger } from './src/logger';

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "graphql-to-mongodb",
3-
"version": "1.4.4",
3+
"version": "1.5.0",
44
"description": "Allows for generic run-time generation of filter types for existing graphql types and parsing client requests to mongodb find queries",
55
"main": "lib/index.js",
66
"types": "lib/index.d.ts",

src/common.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,18 @@ export function isListType(graphQLType: GraphQLType): boolean {
111111
return false;
112112
}
113113

114+
export function isNonNullType(graphQLType: GraphQLType): boolean {
115+
let innerType = graphQLType;
116+
117+
while (innerType instanceof GraphQLList
118+
|| innerType instanceof GraphQLNonNull) {
119+
if (innerType instanceof GraphQLNonNull) return true;
120+
innerType = innerType.ofType;
121+
}
122+
123+
return false;
124+
}
125+
114126
export function isScalarType(graphQLType: GraphQLType): boolean {
115127
return graphQLType instanceof GraphQLScalarType || graphQLType instanceof GraphQLEnumType;
116128
}

src/graphQLMutationType.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export function getGraphQLUpdateType(type: GraphQLObjectType, ...excludedFields:
1818
function getUpdateFields(graphQLType: GraphQLObjectType, ...excludedFields: string[]) : () => GraphQLInputFieldConfigMap {
1919
return () => ({
2020
set: { type: getGraphQLInputType(graphQLType, ...excludedFields) },
21-
setOnInsert: { type: getGraphQLInsertTypeNested(graphQLType, ...excludedFields) },
21+
setOnInsert: { type: getGraphQLInputType(graphQLType, ...excludedFields) },
2222
inc: { type: getGraphQLIncType(graphQLType, ...excludedFields) }
2323
});
2424
}

src/mongoDbProjection.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,17 @@ export interface MongoDbProjection {
66
[key: string]: 1
77
};
88

9-
export interface Field {
10-
[key: string]: Field | 1
11-
}
9+
export interface ProjectionField {
10+
[key: string]: ProjectionField | 1
11+
};
1212

1313
interface FieldGraph {
1414
[key: string]: FieldGraph[] | 1
15-
}
15+
};
1616

1717
interface FragmentDictionary {
1818
[key: string]: FieldGraph[]
19-
}
19+
};
2020

2121
export const getMongoDbProjection = logOnError((info: GraphQLResolveInfo, graphQLType: GraphQLObjectType, ...excludedFields: string[]): MongoDbProjection => {
2222
if (!Object.keys(info).includes('fieldNodes')) throw 'First argument of "getMongoDbProjection" must be a GraphQLResolveInfo';
@@ -31,7 +31,7 @@ export const getMongoDbProjection = logOnError((info: GraphQLResolveInfo, graphQ
3131
return mergeProjectionAndResolveDependencies(projection, resolveFieldsDependencies);
3232
});
3333

34-
export function getRequestedFields(info: GraphQLResolveInfo): Field {
34+
export function getRequestedFields(info: GraphQLResolveInfo): ProjectionField {
3535
const selections = flatten(info.fieldNodes.map(_ => _.selectionSet.selections));
3636
const simplifiedNodes = simplifyNodes({ selections: selections }, info);
3737
return mergeNodes(simplifiedNodes);
@@ -72,7 +72,7 @@ function buildFragment(fragmentName: string, info: GraphQLResolveInfo, dictionar
7272
});
7373
}
7474

75-
function mergeNodes(fieldGraphs: FieldGraph[]): Field {
75+
function mergeNodes(fieldGraphs: FieldGraph[]): ProjectionField {
7676
const mergedGraph: FieldGraph = {};
7777

7878
fieldGraphs.forEach(fieldGraph => Object.keys(fieldGraph).forEach(fieldName => {
@@ -93,7 +93,7 @@ function mergeNodes(fieldGraphs: FieldGraph[]): Field {
9393
}, {});
9494
}
9595

96-
export function getProjection(fieldNode: Field, graphQLType: GraphQLObjectType, path: string[] = [], ...excludedFields: string[]): MongoDbProjection {
96+
export function getProjection(fieldNode: ProjectionField, graphQLType: GraphQLObjectType, path: string[] = [], ...excludedFields: string[]): MongoDbProjection {
9797
const typeFields = getTypeFields(graphQLType)();
9898

9999
return Object.assign({}, ...Object.keys(fieldNode)
@@ -112,7 +112,7 @@ export function getProjection(fieldNode: Field, graphQLType: GraphQLObjectType,
112112
}));
113113
}
114114

115-
export function getResolveFieldsDependencies(fieldNode: Field, graphQLType: GraphQLObjectType): string[] {
115+
export function getResolveFieldsDependencies(fieldNode: ProjectionField, graphQLType: GraphQLObjectType): string[] {
116116
const typeFields = getTypeFields(graphQLType)();
117117

118118
return Object.keys(fieldNode)

src/mongoDbUpdate.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,24 @@
11
import { FICTIVE_INC, clear } from './common';
22
import { logOnError } from './logger';
33

4-
export interface updateArg {
4+
export interface UpdateArgs {
55
setOnInsert?: any,
66
set?: any,
77
inc?: any,
88
}
99

10-
export interface updateObj {
11-
update?: any,
10+
export interface UpdateObj {
11+
$setOnInsert?: any
12+
$set?: any
13+
$inc?: any
14+
}
15+
16+
export interface updateParams {
17+
update?: UpdateObj,
1218
options?: any
1319
}
1420

15-
function getMongoDbUpdate(update: updateArg): updateObj {
21+
function getMongoDbUpdate(update: UpdateArgs): updateParams {
1622
return clear({
1723
update: {
1824
$setOnInsert: update.setOnInsert,

src/mongoDbUpdateValidation.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { UpdateArgs } from "./mongoDbUpdate";
2+
import { GraphQLObjectType, GraphQLType, GraphQLNonNull, GraphQLList, GraphQLFieldMap } from "graphql";
3+
import { isNonNullType, getInnerType, flatten, isListType } from "./common";
4+
5+
export interface UpdateField {
6+
[key: string]: UpdateField | UpdateField[] | 1
7+
}
8+
9+
export function validateUpdateArgs(updateArgs: UpdateArgs, graphQLType: GraphQLObjectType): void {
10+
let errors: string[] = [];
11+
12+
errors = [...errors, ...validateNonNullableFields(Object.values(updateArgs), graphQLType)];
13+
14+
if (errors.length > 0) {
15+
throw errors.join("\n");
16+
}
17+
}
18+
19+
export function validateNonNullableFields(objects: object[], graphQLType: GraphQLObjectType, path: string[] = []): string[] {
20+
const typeFields = graphQLType.getFields();
21+
22+
const errors = validateNonNullableFieldsAssert(objects, typeFields, path);
23+
24+
return [...errors, ...validateNonNullableFieldsTraverse(objects, typeFields, path)];
25+
}
26+
27+
export function validateNonNullableFieldsAssert(objects: object[], typeFields: GraphQLFieldMap<any, any>, path: string[] = []): string[] {
28+
return Object
29+
.keys(typeFields)
30+
.map(key => ({ key, type: typeFields[key].type }))
31+
.filter(field => isNonNullType(field.type))
32+
.reduce((agg, field) => {
33+
let fieldPath = [...path, field.key].join(".");
34+
const fieldValues = objects.map(_ => _[field.key]).filter(_ => _ !== undefined);
35+
if (field.type instanceof GraphQLNonNull) {
36+
if (fieldValues.some(_ => _ === null))
37+
return [...agg, `Non-nullable field "${fieldPath}" is set to null`];
38+
if (fieldValues.length === 0)
39+
return [...agg, `Missing non-nullable field "${fieldPath}"`];
40+
}
41+
if (isListType(field.type) && !validateNonNullListField(fieldValues, field.type)) {
42+
return [...agg, `Non-nullable element of array "${fieldPath}" is set to null`];
43+
}
44+
45+
return agg;
46+
}, []);
47+
}
48+
49+
export function validateNonNullListField(fieldValues: object[], type: GraphQLType): boolean {
50+
if (type instanceof GraphQLNonNull) {
51+
if (fieldValues.some(_ => _ === null)) {
52+
return false;
53+
}
54+
55+
return validateNonNullListField(fieldValues, type.ofType);
56+
}
57+
58+
if (type instanceof GraphQLList) {
59+
return validateNonNullListField(flatten(fieldValues.filter(_ => _) as object[][]), type.ofType);
60+
}
61+
62+
return true;
63+
}
64+
65+
export function validateNonNullableFieldsTraverse(objects: object[], typeFields: GraphQLFieldMap<any, any>, path: string[] = []): string[] {
66+
let keys: string[] = Array.from(new Set(flatten(objects.map(_ => Object.keys(_)))));
67+
68+
return keys.reduce((agg, key) => {
69+
const field = typeFields[key];
70+
const type = field.type;
71+
const innerType = getInnerType(type);
72+
73+
if (!(innerType instanceof GraphQLObjectType) || field.resolve) {
74+
return agg;
75+
}
76+
77+
const newPath = [...path, key];
78+
const values = objects.map(_ => _[key]).filter(_ => _);
79+
80+
if (isListType(type)) {
81+
return [...agg, ...flatten(flattenListField(values, type).map(_ => validateNonNullableFields([_], innerType, newPath)))];
82+
} else {
83+
return [...agg, ...validateNonNullableFields(values, innerType, newPath)];
84+
}
85+
}, []);
86+
}
87+
88+
export function flattenListField(objects: object[], type: GraphQLType): object[] {
89+
if (type instanceof GraphQLNonNull) {
90+
return flattenListField(objects, type.ofType);
91+
}
92+
93+
if (type instanceof GraphQLList) {
94+
return flattenListField(flatten(objects as object[][]).filter(_ => _), type.ofType);
95+
}
96+
97+
return objects;
98+
}

src/queryResolver.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,10 @@ const defaultOptions: QueryOptions = {
3333
differentOutputType: false
3434
};
3535

36-
export function getMongoDbQueryResolver<TSource, TContext>(graphQLType: GraphQLObjectType, queryCallback: QueryCallback<TSource, TContext>, queryOptions: QueryOptions = defaultOptions)
37-
: GraphQLFieldResolver<TSource, TContext> {
36+
export function getMongoDbQueryResolver<TSource, TContext>(
37+
graphQLType: GraphQLObjectType,
38+
queryCallback: QueryCallback<TSource, TContext>,
39+
queryOptions: QueryOptions = defaultOptions): GraphQLFieldResolver<TSource, TContext> {
3840
if (!isType(graphQLType)) throw 'getMongoDbQueryResolver must recieve a graphql type'
3941
if (typeof queryCallback !== 'function') throw 'getMongoDbQueryResolver must recieve a queryCallback function'
4042

src/updateResolver.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { getGraphQLFilterType } from './graphQLFilterType';
22
import { getGraphQLUpdateType } from './graphQLMutationType';
33
import getMongoDbFilter from './mongoDbFilter';
44
import getMongoDbUpdate from './mongoDbUpdate';
5+
import { validateUpdateArgs } from './mongoDbUpdateValidation';
56
import { GraphQLNonNull, isType, GraphQLResolveInfo, GraphQLFieldResolver, GraphQLObjectType } from 'graphql';
67
import { getMongoDbProjection, MongoDbProjection } from './mongoDbProjection';
78

@@ -19,20 +20,25 @@ export interface UpdateCallback<TSource, TContext> {
1920
};
2021

2122
export interface UpdateOptions {
22-
differentOutputType: boolean
23+
differentOutputType: boolean;
24+
validateUpdateArgs: boolean;
2325
};
2426

2527
const defaultOptions: UpdateOptions = {
26-
differentOutputType: false
28+
differentOutputType: false,
29+
validateUpdateArgs: false
2730
};
2831

29-
export function getMongoDbUpdateResolver<TSource, TContext>(graphQLType: GraphQLObjectType, updateCallback: UpdateCallback<TSource, TContext>, updateOptions: UpdateOptions = defaultOptions)
30-
: GraphQLFieldResolver<TSource, TContext> {
32+
export function getMongoDbUpdateResolver<TSource, TContext>(
33+
graphQLType: GraphQLObjectType,
34+
updateCallback: UpdateCallback<TSource, TContext>,
35+
updateOptions: UpdateOptions = defaultOptions): GraphQLFieldResolver<TSource, TContext> {
3136
if (!isType(graphQLType)) throw 'getMongoDbUpdateResolver must recieve a graphql type';
3237
if (typeof updateCallback !== 'function') throw 'getMongoDbUpdateResolver must recieve an updateCallback';
3338

3439
return async (source: TSource, args: { [argName: string]: any }, context: TContext, info: GraphQLResolveInfo): Promise<any> => {
3540
const filter = getMongoDbFilter(graphQLType, args.filter);
41+
if (updateOptions.validateUpdateArgs) validateUpdateArgs(args.update, graphQLType);
3642
const mongoUpdate = getMongoDbUpdate(args.update);
3743
const projection = updateOptions.differentOutputType ? undefined : getMongoDbProjection(info, graphQLType);
3844
return await updateCallback(filter, mongoUpdate.update, mongoUpdate.options, projection, source, args, context, info);

tests/specs/mongoDbProjection.spec.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { getMongoDbProjection, getRequestedFields, getProjection, getResolveFieldsDependencies, mergeProjectionAndResolveDependencies, Field, MongoDbProjection } from "../../src/mongoDbProjection";
1+
import { getMongoDbProjection, getRequestedFields, getProjection, getResolveFieldsDependencies, mergeProjectionAndResolveDependencies, ProjectionField, MongoDbProjection } from "../../src/mongoDbProjection";
22
import { ObjectType } from "../utils/types";
33
import fieldResolve from "../utils/fieldResolve";
44
import { expect } from "chai";
@@ -169,7 +169,7 @@ describe("mongoDbProjection", () => {
169169
}));
170170
});
171171
describe("getRequestedFields", () => {
172-
const tests: { description: string, query: string, expecteFields: Field }[] = [
172+
const tests: { description: string, query: string, expecteFields: ProjectionField }[] = [
173173
{
174174
description: "Should get fields of a simple request",
175175
query: queries.simple,
@@ -249,7 +249,7 @@ describe("mongoDbProjection", () => {
249249
}));
250250
});
251251
describe("getProjection", () => {
252-
const tests: { description: string, fieldNode: Field, expectedProjection: MongoDbProjection }[] = [
252+
const tests: { description: string, fieldNode: ProjectionField, expectedProjection: MongoDbProjection }[] = [
253253
{
254254
description: "Should get projection of a simple request",
255255
fieldNode: {
@@ -331,7 +331,7 @@ describe("mongoDbProjection", () => {
331331
}));
332332
});
333333
describe("getResolveFieldsDependencies", () => {
334-
const tests: { description: string, fieldNode: Field, expectedDependencies: string[] }[] = [{
334+
const tests: { description: string, fieldNode: ProjectionField, expectedDependencies: string[] }[] = [{
335335
description: "Should get resolve fields dependencies of scalars",
336336
fieldNode: {
337337
resolveSpecificDependencies: 1
@@ -411,6 +411,7 @@ describe("mongoDbProjection", () => {
411411
projection: {
412412
"stringScalar": 1,
413413
"nested.floatScalar": 1,
414+
"nested.stringScalar": 1,
414415
"nestedList.intScalar": 1,
415416
"someNested": 1
416417
},

0 commit comments

Comments
 (0)