-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement db lists API in terms of GraphQLSchema (#5569)
- Loading branch information
Showing
5 changed files
with
298 additions
and
202 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'@keystone-next/keystone': patch | ||
--- | ||
|
||
Refactored implementation of db lists API |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
209 changes: 209 additions & 0 deletions
209
packages-next/keystone/src/lib/context/executeGraphQLFieldToRootVal.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,209 @@ | ||
import { KeystoneContext } from '@keystone-next/types'; | ||
import { | ||
GraphQLScalarType, | ||
GraphQLObjectType, | ||
GraphQLArgument, | ||
GraphQLArgumentConfig, | ||
GraphQLSchema, | ||
DocumentNode, | ||
validate, | ||
execute, | ||
GraphQLField, | ||
VariableDefinitionNode, | ||
TypeNode, | ||
GraphQLType, | ||
GraphQLNonNull, | ||
GraphQLList, | ||
GraphQLEnumType, | ||
GraphQLInputObjectType, | ||
GraphQLInterfaceType, | ||
GraphQLUnionType, | ||
ListTypeNode, | ||
NamedTypeNode, | ||
ArgumentNode, | ||
GraphQLFieldConfig, | ||
GraphQLOutputType, | ||
} from 'graphql'; | ||
|
||
function getNamedOrListTypeNodeForType( | ||
type: | ||
| GraphQLScalarType | ||
| GraphQLObjectType<any, any> | ||
| GraphQLInterfaceType | ||
| GraphQLUnionType | ||
| GraphQLEnumType | ||
| GraphQLInputObjectType | ||
| GraphQLList<any> | ||
): NamedTypeNode | ListTypeNode { | ||
if (type instanceof GraphQLList) { | ||
return { kind: 'ListType', type: getTypeNodeForType(type.ofType) }; | ||
} | ||
return { kind: 'NamedType', name: { kind: 'Name', value: type.name } }; | ||
} | ||
|
||
function getTypeNodeForType(type: GraphQLType): TypeNode { | ||
if (type instanceof GraphQLNonNull) { | ||
return { kind: 'NonNullType', type: getNamedOrListTypeNodeForType(type.ofType) }; | ||
} | ||
return getNamedOrListTypeNodeForType(type); | ||
} | ||
|
||
function getVariablesForGraphQLField(field: GraphQLField<any, any>) { | ||
const variableDefinitions: VariableDefinitionNode[] = field.args.map(arg => ({ | ||
kind: 'VariableDefinition', | ||
type: getTypeNodeForType(arg.type), | ||
variable: { kind: 'Variable', name: { kind: 'Name', value: arg.name } }, | ||
})); | ||
|
||
const argumentNodes: ArgumentNode[] = field.args.map(arg => ({ | ||
kind: 'Argument', | ||
name: { kind: 'Name', value: arg.name }, | ||
value: { kind: 'Variable', name: { kind: 'Name', value: arg.name } }, | ||
})); | ||
|
||
return { variableDefinitions, argumentNodes }; | ||
} | ||
|
||
const rawField = 'raw'; | ||
|
||
const RawScalar = new GraphQLScalarType({ name: 'RawThingPlsDontRelyOnThisAnywhere' }); | ||
|
||
const ReturnRawValueObjectType = new GraphQLObjectType({ | ||
name: 'ReturnRawValue', | ||
fields: { | ||
[rawField]: { | ||
type: RawScalar, | ||
resolve(rootVal) { | ||
return rootVal; | ||
}, | ||
}, | ||
}, | ||
}); | ||
|
||
type RequiredButStillAllowUndefined< | ||
T extends Record<string, any>, | ||
// this being a param is important and is what makes this work, | ||
// please do not move it inside the mapped type. | ||
// i can't find a place that explains this but the tldr is that | ||
// having the keyof T _inside_ the mapped type means TS will keep modifiers | ||
// like readonly and optionality and we want to remove those here | ||
Key extends keyof T = keyof T | ||
> = { | ||
[K in Key]: T[K]; | ||
}; | ||
|
||
function argsToArgsConfig(args: GraphQLArgument[]) { | ||
return Object.fromEntries( | ||
args.map(arg => { | ||
const argConfig: RequiredButStillAllowUndefined<GraphQLArgumentConfig> = { | ||
astNode: arg.astNode, | ||
defaultValue: arg.defaultValue, | ||
deprecationReason: arg.deprecationReason, | ||
description: arg.description, | ||
extensions: arg.extensions, | ||
type: arg.type, | ||
}; | ||
return [arg.name, argConfig]; | ||
}) | ||
); | ||
} | ||
|
||
type OutputTypeWithoutNonNull = GraphQLObjectType | GraphQLList<OutputType>; | ||
|
||
type OutputType = OutputTypeWithoutNonNull | GraphQLNonNull<OutputTypeWithoutNonNull>; | ||
|
||
// note the GraphQLNonNull and GraphQLList constructors are incorrectly | ||
// not generic over their inner type which is why we have to use as | ||
// (the classes are generic but not the constructors) | ||
function getTypeForField(originalType: GraphQLOutputType): OutputType { | ||
if (originalType instanceof GraphQLNonNull) { | ||
return new GraphQLNonNull(getTypeForField(originalType.ofType)) as OutputType; | ||
} | ||
if (originalType instanceof GraphQLList) { | ||
return new GraphQLList(getTypeForField(originalType.ofType)) as OutputType; | ||
} | ||
return ReturnRawValueObjectType; | ||
} | ||
|
||
function getRootValGivenOutputType(originalType: OutputType, value: any): any { | ||
if (originalType instanceof GraphQLNonNull) { | ||
return getRootValGivenOutputType(originalType.ofType, value); | ||
} | ||
if (originalType instanceof GraphQLList) { | ||
if (value === null) return null; | ||
return value.map((x: any) => getRootValGivenOutputType(originalType.ofType, x)); | ||
} | ||
return value[rawField]; | ||
} | ||
|
||
export function executeGraphQLFieldToRootVal(field: GraphQLField<any, any>) { | ||
const { argumentNodes, variableDefinitions } = getVariablesForGraphQLField(field); | ||
const document: DocumentNode = { | ||
kind: 'Document', | ||
definitions: [ | ||
{ | ||
kind: 'OperationDefinition', | ||
operation: 'query', | ||
selectionSet: { | ||
kind: 'SelectionSet', | ||
selections: [ | ||
{ | ||
kind: 'Field', | ||
name: { kind: 'Name', value: field.name }, | ||
arguments: argumentNodes, | ||
selectionSet: { | ||
kind: 'SelectionSet', | ||
selections: [{ kind: 'Field', name: { kind: 'Name', value: rawField } }], | ||
}, | ||
}, | ||
], | ||
}, | ||
variableDefinitions, | ||
}, | ||
], | ||
}; | ||
|
||
const type = getTypeForField(field.type); | ||
|
||
const fieldConfig: RequiredButStillAllowUndefined<GraphQLFieldConfig<any, any>> = { | ||
args: argsToArgsConfig(field.args), | ||
astNode: undefined, | ||
deprecationReason: field.deprecationReason, | ||
description: field.description, | ||
extensions: field.extensions, | ||
resolve: field.resolve, | ||
subscribe: field.subscribe, | ||
type, | ||
}; | ||
const schema = new GraphQLSchema({ | ||
query: new GraphQLObjectType({ | ||
name: 'Query', | ||
fields: { | ||
[field.name]: fieldConfig, | ||
}, | ||
}), | ||
}); | ||
|
||
const validationErrors = validate(schema, document); | ||
|
||
if (validationErrors.length > 0) { | ||
throw validationErrors[0]; | ||
} | ||
return async ( | ||
args: Record<string, any>, | ||
context: KeystoneContext, | ||
rootValue: Record<string, string> = {} | ||
) => { | ||
const result = await execute({ | ||
schema, | ||
document, | ||
contextValue: context, | ||
variableValues: args, | ||
rootValue, | ||
}); | ||
if (result.errors?.length) { | ||
throw result.errors[0]; | ||
} | ||
return getRootValGivenOutputType(type, result.data![field.name]); | ||
}; | ||
} |
75 changes: 0 additions & 75 deletions
75
packages-next/keystone/src/lib/context/getCoerceAndValidateArgumentsFnForGraphQLField.ts
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.
d216fd0
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Successfully deployed to the following URLs: