Skip to content

Commit

Permalink
Merge branch 'master' into document-field-updates
Browse files Browse the repository at this point in the history
  • Loading branch information
emmatown authored Sep 15, 2021
2 parents 0cc27fa + 1faddea commit 5aa8271
Show file tree
Hide file tree
Showing 19 changed files with 54 additions and 39 deletions.
5 changes: 5 additions & 0 deletions .changeset/gentle-wolves-join.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@keystone-next/keystone': patch
---

Updated error messages to indicate user input errors.
2 changes: 1 addition & 1 deletion examples-staging/ecommerce/tests/mutations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ describe(`Custom mutations`, () => {
});
expect(data).toEqual({ addToCart: null });
expect(errors).toHaveLength(1);
expect(errors![0].message).toEqual('Only a cuid can be passed to id filters');
expect(errors![0].message).toEqual('Input error: Only a cuid can be passed to id filters');
})
);

Expand Down
5 changes: 3 additions & 2 deletions packages/keystone/src/fields/types/checkbox/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { userInputError } from '../../../lib/core/graphql-errors';
import {
BaseGeneratedListTypes,
CommonFieldConfig,
Expand Down Expand Up @@ -50,7 +51,7 @@ export const checkbox =
}),
resolve(val) {
if (val === null) {
throw new Error('checkbox fields cannot be set to null');
throw userInputError('checkbox fields cannot be set to null');
}
return val ?? defaultValue;
},
Expand All @@ -59,7 +60,7 @@ export const checkbox =
arg: graphql.arg({ type: graphql.Boolean }),
resolve(val) {
if (val === null) {
throw new Error('checkbox fields cannot be set to null');
throw userInputError('checkbox fields cannot be set to null');
}
return val;
},
Expand Down
5 changes: 3 additions & 2 deletions packages/keystone/src/fields/types/file/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { FileUpload } from 'graphql-upload';
import { userInputError } from '../../../lib/core/graphql-errors';
import {
fieldType,
graphql,
Expand Down Expand Up @@ -68,12 +69,12 @@ async function inputResolver(data: FileFieldInputType, context: KeystoneContext)

if (data.ref) {
if (data.upload) {
throw new Error('Only one of ref and upload can be passed to FileFieldInput');
throw userInputError('Only one of ref and upload can be passed to FileFieldInput');
}
return context.files!.getDataFromRef(data.ref);
}
if (!data.upload) {
throw new Error('Either ref or upload must be passed to FileFieldInput');
throw userInputError('Either ref or upload must be passed to FileFieldInput');
}
const upload = await data.upload;
return context.files!.getDataFromStream(upload.createReadStream(), upload.filename);
Expand Down
5 changes: 3 additions & 2 deletions packages/keystone/src/fields/types/image/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { FileUpload } from 'graphql-upload';
import { userInputError } from '../../../lib/core/graphql-errors';
import {
BaseGeneratedListTypes,
fieldType,
Expand Down Expand Up @@ -75,12 +76,12 @@ async function inputResolver(data: ImageFieldInputType, context: KeystoneContext

if (data.ref) {
if (data.upload) {
throw new Error('Only one of ref and upload can be passed to ImageFieldInput');
throw userInputError('Only one of ref and upload can be passed to ImageFieldInput');
}
return context.images!.getDataFromRef(data.ref);
}
if (!data.upload) {
throw new Error('Either ref or upload must be passed to ImageFieldInput');
throw userInputError('Either ref or upload must be passed to ImageFieldInput');
}
return context.images!.getDataFromStream((await data.upload).createReadStream());
}
Expand Down
3 changes: 2 additions & 1 deletion packages/keystone/src/fields/types/password/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import bcryptjs from 'bcryptjs';
// @ts-ignore
import dumbPasswords from 'dumb-passwords';
import { userInputError } from '../../../lib/core/graphql-errors';
import {
BaseGeneratedListTypes,
FieldDefaultValue,
Expand Down Expand Up @@ -100,7 +101,7 @@ export const password =
arg: graphql.arg({ type: PasswordFilter }),
resolve(val) {
if (val === null) {
throw new Error('Password filters cannot be set to null');
throw userInputError('Password filters cannot be set to null');
}
if (val.isSet) {
return {
Expand Down
2 changes: 2 additions & 0 deletions packages/keystone/src/lib/core/graphql-errors.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { ApolloError } from 'apollo-server-errors';

export const userInputError = (msg: string) => new ApolloError(`Input error: ${msg}`);

export const accessDeniedError = () => new ApolloError('You do not have access to this resource');

export const prismaError = (err: Error) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { KeystoneContext, TypesForList, graphql } from '../../../types';
import { resolveUniqueWhereInput, UniqueInputFilter, UniquePrismaFilter } from '../where-inputs';
import { InitialisedList } from '../types-for-lists';
import { isRejected, isFulfilled } from '../utils';
import { userInputError } from '../graphql-errors';
import { NestedMutationState } from './create-update';

type _CreateValueType = Exclude<
Expand Down Expand Up @@ -43,7 +44,7 @@ export function resolveRelateToManyForCreateInput(
) {
return async (value: _CreateValueType) => {
if (!Array.isArray(value.connect) && !Array.isArray(value.create)) {
throw new Error(
throw userInputError(
`You must provide at least one field in to-many relationship inputs but none were provided at ${target}`
);
}
Expand Down Expand Up @@ -90,12 +91,12 @@ export function resolveRelateToManyForUpdateInput(
!Array.isArray(value.disconnect) &&
!Array.isArray(value.set)
) {
throw new Error(
throw userInputError(
`You must provide at least one field in to-many relationship inputs but none were provided at ${target}`
);
}
if (value.set && value.disconnect) {
throw new Error(
throw userInputError(
`The set and disconnect fields cannot both be provided to to-many relationship inputs but both were provided at ${target}`
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { KeystoneContext, TypesForList, graphql } from '../../../types';
import { resolveUniqueWhereInput } from '../where-inputs';
import { InitialisedList } from '../types-for-lists';
import { userInputError } from '../graphql-errors';
import { NestedMutationState } from './create-update';

type _CreateValueType = Exclude<
Expand Down Expand Up @@ -60,7 +61,7 @@ export function resolveRelateToOneForCreateInput(
return async (value: _CreateValueType) => {
const numOfKeys = Object.keys(value).length;
if (numOfKeys !== 1) {
throw new Error(
throw userInputError(
`Nested to-one mutations must provide exactly one field if they're provided but ${target} did not`
);
}
Expand All @@ -76,7 +77,7 @@ export function resolveRelateToOneForUpdateInput(
) {
return async (value: _UpdateValueType) => {
if (Object.keys(value).length !== 1) {
throw new Error(
throw userInputError(
`Nested to-one mutations must provide exactly one field if they're provided but ${target} did not`
);
}
Expand Down
6 changes: 3 additions & 3 deletions packages/keystone/src/lib/core/queries/resolvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
UniqueInputFilter,
InputFilter,
} from '../where-inputs';
import { limitsExceededError } from '../graphql-errors';
import { limitsExceededError, userInputError } from '../graphql-errors';
import { InitialisedList } from '../types-for-lists';
import { getDBFieldKeyForFieldOnMultiField, runWithPrisma } from '../utils';

Expand Down Expand Up @@ -132,15 +132,15 @@ async function resolveOrderBy(
orderBy.map(async orderBySelection => {
const keys = Object.keys(orderBySelection);
if (keys.length !== 1) {
throw new Error(
throw userInputError(
`Only a single key must be passed to ${list.types.orderBy.graphQLType.name}`
);
}

const fieldKey = keys[0];
const value = orderBySelection[fieldKey];
if (value === null) {
throw new Error('null cannot be passed as an order direction');
throw userInputError('null cannot be passed as an order direction');
}

const field = list.fields[fieldKey];
Expand Down
9 changes: 5 additions & 4 deletions packages/keystone/src/lib/core/where-inputs.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { DBField, KeystoneContext } from '../../types';
import { userInputError } from './graphql-errors';
import { InitialisedList } from './types-for-lists';
import { getDBFieldKeyForFieldOnMultiField } from './utils';

Expand Down Expand Up @@ -33,14 +34,14 @@ export async function resolveUniqueWhereInput(
): Promise<UniquePrismaFilter> {
const inputKeys = Object.keys(input);
if (inputKeys.length !== 1) {
throw new Error(
throw userInputError(
`Exactly one key must be passed in a unique where input but ${inputKeys.length} keys were passed`
);
}
const key = inputKeys[0];
const val = input[key];
if (val === null) {
throw new Error(`The unique value provided in a unique where input must not be null`);
throw userInputError(`The unique value provided in a unique where input must not be null`);
}
const resolver = fields[key].input!.uniqueWhere!.resolve;
return { [key]: resolver ? await resolver(val, context) : val };
Expand Down Expand Up @@ -79,13 +80,13 @@ export async function resolveWhereInput(
if (field.dbField.mode === 'many') {
return async () => {
if (value === null) {
throw new Error('A many relation filter cannot be set to null');
throw userInputError('A many relation filter cannot be set to null');
}
return Object.fromEntries(
await Promise.all(
Object.entries(value).map(async ([key, val]) => {
if (val === null) {
throw new Error(
throw userInputError(
`The key "${key}" in a many relation filter cannot be set to null`
);
}
Expand Down
13 changes: 7 additions & 6 deletions packages/keystone/src/lib/id-field.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
graphql,
} from '../types';
import { packagePath } from '../package-path';
import { userInputError } from './core/graphql-errors';

const views = path.join(
packagePath,
Expand All @@ -19,27 +20,27 @@ const views = path.join(
const idParsers = {
autoincrement(val: string | null) {
if (val === null) {
throw new Error('Only an integer can be passed to id filters');
throw userInputError('Only an integer can be passed to id filters');
}
const parsed = parseInt(val);
if (Number.isInteger(parsed)) {
return parsed;
}
throw new Error('Only an integer can be passed to id filters');
throw userInputError('Only an integer can be passed to id filters');
},
cuid(val: string | null) {
// isCuid is just "it's a string and it starts with c"
// https://github.com/ericelliott/cuid/blob/215b27bdb78d3400d4225a4eeecb3b71891a5f6f/index.js#L69-L73
if (typeof val === 'string' && isCuid(val)) {
return val;
}
throw new Error('Only a cuid can be passed to id filters');
throw userInputError('Only a cuid can be passed to id filters');
},
uuid(val: string | null) {
if (typeof val === 'string' && validate(val)) {
return val.toLowerCase();
}
throw new Error('Only a uuid can be passed to id filters');
throw userInputError('Only a uuid can be passed to id filters');
},
};

Expand Down Expand Up @@ -74,7 +75,7 @@ function resolveVal(
kind: IdFieldConfig['kind']
): any {
if (input === null) {
throw new Error('id filter cannot be null');
throw userInputError('id filter cannot be null');
}
const idParser = idParsers[kind];
const obj: any = {};
Expand All @@ -89,7 +90,7 @@ function resolveVal(
const val = input[key];
if (val !== undefined) {
if (val === null) {
throw new Error(`${key} id filter cannot be null`);
throw userInputError(`${key} id filter cannot be null`);
}
obj[key] = val.map(x => idParser(x));
}
Expand Down
12 changes: 6 additions & 6 deletions tests/api-tests/queries/filters.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { text, relationship } from '@keystone-next/keystone/fields';
import { list } from '@keystone-next/keystone';
import { setupTestRunner } from '@keystone-next/keystone/testing';
import { apiTestConfig, expectInternalServerError } from '../utils';
import { apiTestConfig, expectBadUserInput } from '../utils';

const runner = setupTestRunner({
config: apiTestConfig({
Expand Down Expand Up @@ -80,7 +80,7 @@ describe('filtering on relationships', () => {
});
// Returns null and throws an error
expect(body.data).toEqual({ secondaryLists: null });
expectInternalServerError(body.errors, false, [
expectBadUserInput(body.errors, [
{
message: 'A many relation filter cannot be set to null',
path: ['secondaryLists'],
Expand All @@ -97,7 +97,7 @@ describe('filtering on relationships', () => {
});
// Returns null and throws an error
expect(body.data).toEqual({ secondaryLists: null });
expectInternalServerError(body.errors, false, [
expectBadUserInput(body.errors, [
{
message: 'The key "some" in a many relation filter cannot be set to null',
path: ['secondaryLists'],
Expand Down Expand Up @@ -144,7 +144,7 @@ describe('searching by unique fields', () => {
const { body } = await graphQLRequest({ query: '{ user(where: {}) { id email } }' });
// Returns null and throws an error
expect(body.data).toEqual({ user: null });
expectInternalServerError(body.errors, false, [
expectBadUserInput(body.errors, [
{
message: 'Exactly one key must be passed in a unique where input but 0 keys were passed',
path: ['user'],
Expand All @@ -162,7 +162,7 @@ describe('searching by unique fields', () => {
});
// Returns null and throws an error
expect(body.data).toEqual({ user: null });
expectInternalServerError(body.errors, false, [
expectBadUserInput(body.errors, [
{
message: 'Exactly one key must be passed in a unique where input but 2 keys were passed',
path: ['user'],
Expand All @@ -179,7 +179,7 @@ describe('searching by unique fields', () => {
});
// Returns null and throws an error
expect(body.data).toEqual({ user: null });
expectInternalServerError(body.errors, false, [
expectBadUserInput(body.errors, [
{
message: 'The unique value provided in a unique where input must not be null',
path: ['user'],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@ describe('non-matching filter', () => {
{
path: ['updateUser'],
message:
'You must provide at least one field in to-many relationship inputs but none were provided at User.notes<Note>',
'Input error: You must provide at least one field in to-many relationship inputs but none were provided at User.notes<Note>',
},
]);
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ describe('non-matching filter', () => {
expectRelationshipError(errors, [
{
path: ['updateEvent'],
message: `Nested to-one mutations must provide exactly one field if they're provided but Event.group<Group> did not`,
message: `Input error: Nested to-one mutations must provide exactly one field if they're provided but Event.group<Group> did not`,
},
]);
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ describe('errors on incomplete data', () => {
{
path: ['createUser'],
message:
'You must provide at least one field in to-many relationship inputs but none were provided at User.notes<Note>',
'Input error: You must provide at least one field in to-many relationship inputs but none were provided at User.notes<Note>',
},
]);
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ describe('errors on incomplete data', () => {
{
path: ['createEvent'],
message:
"Nested to-one mutations must provide exactly one field if they're provided but Event.group<Group> did not",
"Input error: Nested to-one mutations must provide exactly one field if they're provided but Event.group<Group> did not",
},
]);
})
Expand All @@ -67,7 +67,7 @@ describe('errors on incomplete data', () => {
{
path: ['createEvent'],
message:
"Nested to-one mutations must provide exactly one field if they're provided but Event.group<Group> did not",
"Input error: Nested to-one mutations must provide exactly one field if they're provided but Event.group<Group> did not",
},
]);
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ describe('no access control', () => {
{
path: ['updateUser'],
message:
'The set and disconnect fields cannot both be provided to to-many relationship inputs but both were provided at User.notes<Note>',
'Input error: The set and disconnect fields cannot both be provided to to-many relationship inputs but both were provided at User.notes<Note>',
},
]);
})
Expand Down
Loading

0 comments on commit 5aa8271

Please sign in to comment.