Skip to content

Commit

Permalink
1.5.0 (OS-Guild#31)
Browse files Browse the repository at this point in the history
  • Loading branch information
yoavkarako authored Oct 8, 2018
1 parent 86f0df7 commit b6e53af
Show file tree
Hide file tree
Showing 14 changed files with 432 additions and 58 deletions.
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
# Change Log
### 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...)
- Tests! (Limited coverage)
---
##### 1.4.4
- Bug fix
---
Expand Down Expand Up @@ -30,7 +35,7 @@
- Fix type declarations in package
---
### 1.3.0
- Renameed package and repository
- Renamed package and repository
- TypeScript!
- Log warn and error callbacks
---
Expand Down
42 changes: 12 additions & 30 deletions index.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,12 @@
import { getGraphQLFilterType } from './src/graphQLFilterType';
import getMongoDbFilter from './src/mongoDbFilter';
import { getGraphQLUpdateType, getGraphQLInsertType } from './src/graphQLMutationType';
import getMongoDbUpdate from './src/mongoDbUpdate';
import GraphQLPaginationType from './src/graphQLPaginationType';
import getGraphQLSortType from './src/graphQLSortType';
import getMongoDbSort from './src/mongoDbSort';
import { getMongoDbProjection } from './src/mongoDbProjection';
import { getMongoDbQueryResolver, getGraphQLQueryArgs, QueryOptions } from './src/queryResolver';
import { getMongoDbUpdateResolver, getGraphQLUpdateArgs, UpdateOptions } from './src/updateResolver';
import { setLogger } from './src/logger';

export {
getGraphQLFilterType,
getMongoDbFilter,
getGraphQLUpdateType,
getGraphQLInsertType,
getMongoDbUpdate,
GraphQLPaginationType,
getGraphQLSortType,
getMongoDbSort,
getMongoDbProjection,
QueryOptions,
getMongoDbQueryResolver,
getGraphQLQueryArgs,
UpdateOptions,
getMongoDbUpdateResolver,
getGraphQLUpdateArgs,
setLogger
};
export { getGraphQLFilterType } from './src/graphQLFilterType';
export { default as getMongoDbFilter } from './src/mongoDbFilter';
export { getGraphQLUpdateType, getGraphQLInsertType } from './src/graphQLMutationType';
export { default as getMongoDbUpdate } from './src/mongoDbUpdate';
export { validateUpdateArgs } from "./src/mongoDbUpdateValidation";
export { default as GraphQLPaginationType } from './src/graphQLPaginationType';
export { default as getGraphQLSortType } from './src/graphQLSortType';
export { default as getMongoDbSort } from './src/mongoDbSort';
export { getMongoDbProjection } from './src/mongoDbProjection';
export { getMongoDbQueryResolver, getGraphQLQueryArgs, QueryOptions } from './src/queryResolver';
export { getMongoDbUpdateResolver, getGraphQLUpdateArgs, UpdateOptions } from './src/updateResolver';
export { setLogger } from './src/logger';
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.4.4",
"version": "1.5.0",
"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
12 changes: 12 additions & 0 deletions src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,18 @@ export function isListType(graphQLType: GraphQLType): boolean {
return false;
}

export function isNonNullType(graphQLType: GraphQLType): boolean {
let innerType = graphQLType;

while (innerType instanceof GraphQLList
|| innerType instanceof GraphQLNonNull) {
if (innerType instanceof GraphQLNonNull) return true;
innerType = innerType.ofType;
}

return false;
}

export function isScalarType(graphQLType: GraphQLType): boolean {
return graphQLType instanceof GraphQLScalarType || graphQLType instanceof GraphQLEnumType;
}
Expand Down
2 changes: 1 addition & 1 deletion src/graphQLMutationType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export function getGraphQLUpdateType(type: GraphQLObjectType, ...excludedFields:
function getUpdateFields(graphQLType: GraphQLObjectType, ...excludedFields: string[]) : () => GraphQLInputFieldConfigMap {
return () => ({
set: { type: getGraphQLInputType(graphQLType, ...excludedFields) },
setOnInsert: { type: getGraphQLInsertTypeNested(graphQLType, ...excludedFields) },
setOnInsert: { type: getGraphQLInputType(graphQLType, ...excludedFields) },
inc: { type: getGraphQLIncType(graphQLType, ...excludedFields) }
});
}
Expand Down
18 changes: 9 additions & 9 deletions src/mongoDbProjection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,17 @@ export interface MongoDbProjection {
[key: string]: 1
};

export interface Field {
[key: string]: Field | 1
}
export interface ProjectionField {
[key: string]: ProjectionField | 1
};

interface FieldGraph {
[key: string]: FieldGraph[] | 1
}
};

interface FragmentDictionary {
[key: string]: FieldGraph[]
}
};

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

export function getRequestedFields(info: GraphQLResolveInfo): Field {
export function getRequestedFields(info: GraphQLResolveInfo): ProjectionField {
const selections = flatten(info.fieldNodes.map(_ => _.selectionSet.selections));
const simplifiedNodes = simplifyNodes({ selections: selections }, info);
return mergeNodes(simplifiedNodes);
Expand Down Expand Up @@ -72,7 +72,7 @@ function buildFragment(fragmentName: string, info: GraphQLResolveInfo, dictionar
});
}

function mergeNodes(fieldGraphs: FieldGraph[]): Field {
function mergeNodes(fieldGraphs: FieldGraph[]): ProjectionField {
const mergedGraph: FieldGraph = {};

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

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

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

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

return Object.keys(fieldNode)
Expand Down
14 changes: 10 additions & 4 deletions src/mongoDbUpdate.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
import { FICTIVE_INC, clear } from './common';
import { logOnError } from './logger';

export interface updateArg {
export interface UpdateArgs {
setOnInsert?: any,
set?: any,
inc?: any,
}

export interface updateObj {
update?: any,
export interface UpdateObj {
$setOnInsert?: any
$set?: any
$inc?: any
}

export interface updateParams {
update?: UpdateObj,
options?: any
}

function getMongoDbUpdate(update: updateArg): updateObj {
function getMongoDbUpdate(update: UpdateArgs): updateParams {
return clear({
update: {
$setOnInsert: update.setOnInsert,
Expand Down
98 changes: 98 additions & 0 deletions src/mongoDbUpdateValidation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { UpdateArgs } from "./mongoDbUpdate";
import { GraphQLObjectType, GraphQLType, GraphQLNonNull, GraphQLList, GraphQLFieldMap } from "graphql";
import { isNonNullType, getInnerType, flatten, isListType } from "./common";

export interface UpdateField {
[key: string]: UpdateField | UpdateField[] | 1
}

export function validateUpdateArgs(updateArgs: UpdateArgs, graphQLType: GraphQLObjectType): void {
let errors: string[] = [];

errors = [...errors, ...validateNonNullableFields(Object.values(updateArgs), graphQLType)];

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

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

const errors = validateNonNullableFieldsAssert(objects, typeFields, path);

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

export function validateNonNullableFieldsAssert(objects: object[], typeFields: GraphQLFieldMap<any, any>, path: string[] = []): string[] {
return Object
.keys(typeFields)
.map(key => ({ key, type: typeFields[key].type }))
.filter(field => isNonNullType(field.type))
.reduce((agg, field) => {
let fieldPath = [...path, field.key].join(".");
const fieldValues = objects.map(_ => _[field.key]).filter(_ => _ !== undefined);
if (field.type instanceof GraphQLNonNull) {
if (fieldValues.some(_ => _ === null))
return [...agg, `Non-nullable field "${fieldPath}" is set to null`];
if (fieldValues.length === 0)
return [...agg, `Missing non-nullable field "${fieldPath}"`];
}
if (isListType(field.type) && !validateNonNullListField(fieldValues, field.type)) {
return [...agg, `Non-nullable element of array "${fieldPath}" is set to null`];
}

return agg;
}, []);
}

export function validateNonNullListField(fieldValues: object[], type: GraphQLType): boolean {
if (type instanceof GraphQLNonNull) {
if (fieldValues.some(_ => _ === null)) {
return false;
}

return validateNonNullListField(fieldValues, type.ofType);
}

if (type instanceof GraphQLList) {
return validateNonNullListField(flatten(fieldValues.filter(_ => _) as object[][]), type.ofType);
}

return true;
}

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

return keys.reduce((agg, key) => {
const field = typeFields[key];
const type = field.type;
const innerType = getInnerType(type);

if (!(innerType instanceof GraphQLObjectType) || field.resolve) {
return agg;
}

const newPath = [...path, key];
const values = objects.map(_ => _[key]).filter(_ => _);

if (isListType(type)) {
return [...agg, ...flatten(flattenListField(values, type).map(_ => validateNonNullableFields([_], innerType, newPath)))];
} else {
return [...agg, ...validateNonNullableFields(values, innerType, newPath)];
}
}, []);
}

export function flattenListField(objects: object[], type: GraphQLType): object[] {
if (type instanceof GraphQLNonNull) {
return flattenListField(objects, type.ofType);
}

if (type instanceof GraphQLList) {
return flattenListField(flatten(objects as object[][]).filter(_ => _), type.ofType);
}

return objects;
}
6 changes: 4 additions & 2 deletions src/queryResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,10 @@ const defaultOptions: QueryOptions = {
differentOutputType: false
};

export function getMongoDbQueryResolver<TSource, TContext>(graphQLType: GraphQLObjectType, queryCallback: QueryCallback<TSource, TContext>, queryOptions: QueryOptions = defaultOptions)
: GraphQLFieldResolver<TSource, TContext> {
export function getMongoDbQueryResolver<TSource, TContext>(
graphQLType: GraphQLObjectType,
queryCallback: QueryCallback<TSource, TContext>,
queryOptions: QueryOptions = defaultOptions): GraphQLFieldResolver<TSource, TContext> {
if (!isType(graphQLType)) throw 'getMongoDbQueryResolver must recieve a graphql type'
if (typeof queryCallback !== 'function') throw 'getMongoDbQueryResolver must recieve a queryCallback function'

Expand Down
14 changes: 10 additions & 4 deletions src/updateResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { getGraphQLFilterType } from './graphQLFilterType';
import { getGraphQLUpdateType } from './graphQLMutationType';
import getMongoDbFilter from './mongoDbFilter';
import getMongoDbUpdate from './mongoDbUpdate';
import { validateUpdateArgs } from './mongoDbUpdateValidation';
import { GraphQLNonNull, isType, GraphQLResolveInfo, GraphQLFieldResolver, GraphQLObjectType } from 'graphql';
import { getMongoDbProjection, MongoDbProjection } from './mongoDbProjection';

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

export interface UpdateOptions {
differentOutputType: boolean
differentOutputType: boolean;
validateUpdateArgs: boolean;
};

const defaultOptions: UpdateOptions = {
differentOutputType: false
differentOutputType: false,
validateUpdateArgs: false
};

export function getMongoDbUpdateResolver<TSource, TContext>(graphQLType: GraphQLObjectType, updateCallback: UpdateCallback<TSource, TContext>, updateOptions: UpdateOptions = defaultOptions)
: GraphQLFieldResolver<TSource, TContext> {
export function getMongoDbUpdateResolver<TSource, TContext>(
graphQLType: GraphQLObjectType,
updateCallback: UpdateCallback<TSource, TContext>,
updateOptions: UpdateOptions = defaultOptions): GraphQLFieldResolver<TSource, TContext> {
if (!isType(graphQLType)) throw 'getMongoDbUpdateResolver must recieve a graphql type';
if (typeof updateCallback !== 'function') throw 'getMongoDbUpdateResolver must recieve an updateCallback';

return async (source: TSource, args: { [argName: string]: any }, context: TContext, info: GraphQLResolveInfo): Promise<any> => {
const filter = getMongoDbFilter(graphQLType, args.filter);
if (updateOptions.validateUpdateArgs) validateUpdateArgs(args.update, graphQLType);
const mongoUpdate = getMongoDbUpdate(args.update);
const projection = updateOptions.differentOutputType ? undefined : getMongoDbProjection(info, graphQLType);
return await updateCallback(filter, mongoUpdate.update, mongoUpdate.options, projection, source, args, context, info);
Expand Down
9 changes: 5 additions & 4 deletions tests/specs/mongoDbProjection.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getMongoDbProjection, getRequestedFields, getProjection, getResolveFieldsDependencies, mergeProjectionAndResolveDependencies, Field, MongoDbProjection } from "../../src/mongoDbProjection";
import { getMongoDbProjection, getRequestedFields, getProjection, getResolveFieldsDependencies, mergeProjectionAndResolveDependencies, ProjectionField, MongoDbProjection } from "../../src/mongoDbProjection";
import { ObjectType } from "../utils/types";
import fieldResolve from "../utils/fieldResolve";
import { expect } from "chai";
Expand Down Expand Up @@ -169,7 +169,7 @@ describe("mongoDbProjection", () => {
}));
});
describe("getRequestedFields", () => {
const tests: { description: string, query: string, expecteFields: Field }[] = [
const tests: { description: string, query: string, expecteFields: ProjectionField }[] = [
{
description: "Should get fields of a simple request",
query: queries.simple,
Expand Down Expand Up @@ -249,7 +249,7 @@ describe("mongoDbProjection", () => {
}));
});
describe("getProjection", () => {
const tests: { description: string, fieldNode: Field, expectedProjection: MongoDbProjection }[] = [
const tests: { description: string, fieldNode: ProjectionField, expectedProjection: MongoDbProjection }[] = [
{
description: "Should get projection of a simple request",
fieldNode: {
Expand Down Expand Up @@ -331,7 +331,7 @@ describe("mongoDbProjection", () => {
}));
});
describe("getResolveFieldsDependencies", () => {
const tests: { description: string, fieldNode: Field, expectedDependencies: string[] }[] = [{
const tests: { description: string, fieldNode: ProjectionField, expectedDependencies: string[] }[] = [{
description: "Should get resolve fields dependencies of scalars",
fieldNode: {
resolveSpecificDependencies: 1
Expand Down Expand Up @@ -411,6 +411,7 @@ describe("mongoDbProjection", () => {
projection: {
"stringScalar": 1,
"nested.floatScalar": 1,
"nested.stringScalar": 1,
"nestedList.intScalar": 1,
"someNested": 1
},
Expand Down
Loading

0 comments on commit b6e53af

Please sign in to comment.