graphql.nadel.enginekt.util.GraphQLUtil.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of nadel-engine-nextgen Show documentation
Show all versions of nadel-engine-nextgen Show documentation
Nadel is a Java library that combines multiple GrahpQL services together into one API.
The newest version!
package graphql.nadel.enginekt.util
import graphql.ErrorType
import graphql.ExecutionInput
import graphql.ExecutionResult
import graphql.ExecutionResultImpl
import graphql.ExecutionResultImpl.newExecutionResult
import graphql.GraphQLError
import graphql.GraphqlErrorBuilder.newError
import graphql.GraphqlErrorException
import graphql.execution.ExecutionId
import graphql.execution.ExecutionIdProvider
import graphql.execution.instrumentation.InstrumentationContext
import graphql.execution.instrumentation.InstrumentationState
import graphql.language.ArrayValue
import graphql.language.BooleanValue
import graphql.language.Definition
import graphql.language.Document
import graphql.language.EnumTypeExtensionDefinition
import graphql.language.FloatValue
import graphql.language.ImplementingTypeDefinition
import graphql.language.InputObjectTypeExtensionDefinition
import graphql.language.IntValue
import graphql.language.InterfaceTypeExtensionDefinition
import graphql.language.NamedNode
import graphql.language.Node
import graphql.language.NullValue
import graphql.language.ObjectField
import graphql.language.ObjectTypeExtensionDefinition
import graphql.language.ObjectValue
import graphql.language.OperationDefinition
import graphql.language.SDLDefinition
import graphql.language.ScalarTypeExtensionDefinition
import graphql.language.SchemaExtensionDefinition
import graphql.language.StringValue
import graphql.language.Type
import graphql.language.TypeName
import graphql.language.UnionTypeExtensionDefinition
import graphql.language.Value
import graphql.nadel.NadelOperationKind
import graphql.nadel.ServiceExecutionResult
import graphql.nadel.dsl.UnderlyingServiceHydration
import graphql.nadel.enginekt.transform.query.NadelQueryPath
import graphql.nadel.instrumentation.NadelInstrumentation
import graphql.nadel.instrumentation.parameters.NadelInstrumentationExecuteOperationParameters
import graphql.normalized.ExecutableNormalizedField
import graphql.normalized.ExecutableNormalizedOperation
import graphql.normalized.NormalizedInputValue
import graphql.schema.FieldCoordinates
import graphql.schema.GraphQLFieldDefinition
import graphql.schema.GraphQLFieldsContainer
import graphql.schema.GraphQLInputType
import graphql.schema.GraphQLInterfaceType
import graphql.schema.GraphQLObjectType
import graphql.schema.GraphQLSchema
import graphql.schema.GraphQLType
import graphql.schema.GraphQLTypeUtil
import graphql.schema.GraphQLUnionType
import graphql.schema.GraphQLUnmodifiedType
import graphql.schema.idl.TypeUtil
import kotlinx.coroutines.future.asDeferred
internal typealias AnyAstValue = Value<*>
internal typealias AnyAstNode = Node<*>
internal typealias AnyAstDefinition = Definition<*>
internal typealias AnyImplementingTypeDefinition = ImplementingTypeDefinition<*>
internal typealias AnyNamedNode = NamedNode<*>
internal typealias AnySDLDefinition = SDLDefinition<*>
internal typealias AnyAstType = Type<*>
fun newGraphQLError(
message: String,
errorType: ErrorType,
extensions: MutableJsonMap = mutableMapOf(),
): GraphQLError {
return newError()
.message(message)
.errorType(errorType)
.extensions(extensions)
.build()
}
fun toGraphQLError(
raw: JsonMap,
): GraphQLError {
val errorBuilder = newError()
.message(raw["message"] as String?)
raw["extensions"]?.let { extensions ->
errorBuilder.extensions(extensions as JsonMap)
}
raw["path"]?.let { path ->
errorBuilder.path(path as AnyList)
}
return errorBuilder.build()
}
fun GraphQLSchema.getField(coordinates: FieldCoordinates): GraphQLFieldDefinition? {
return getType(coordinates.typeName)
?.let { it as? GraphQLFieldsContainer }
?.getField(coordinates.fieldName)
}
fun GraphQLSchema.getOperationType(kind: NadelOperationKind): GraphQLObjectType? {
return when (kind) {
NadelOperationKind.Query -> queryType
NadelOperationKind.Mutation -> mutationType
NadelOperationKind.Subscription -> subscriptionType
}
}
fun GraphQLFieldsContainer.getFieldAt(
pathToField: List,
): GraphQLFieldDefinition? {
return getFieldAt(pathToField, pathIndex = 0)
}
fun GraphQLFieldsContainer.getFieldsAlong(
pathToField: List,
): List {
var parent = this
return pathToField.mapIndexed { index, fieldName ->
val field = parent.getField(fieldName)
if (index != pathToField.lastIndex) {
parent = field.type.unwrapAll() as GraphQLFieldsContainer
}
field
}
}
private fun GraphQLFieldsContainer.getFieldAt(
pathToField: List,
pathIndex: Int,
): GraphQLFieldDefinition? {
val field = getField(pathToField[pathIndex]) ?: return null
return if (pathIndex == pathToField.lastIndex) {
field
} else {
val fieldOutputType = field.type.unwrapAll() as GraphQLFieldsContainer
fieldOutputType.getFieldAt(pathToField, pathIndex + 1)
}
}
fun ExecutableNormalizedField.toBuilder(): ExecutableNormalizedField.Builder {
var builder: ExecutableNormalizedField.Builder? = null
transform { builder = it }
return builder!!
}
fun ExecutableNormalizedField.copyWithChildren(children: List): ExecutableNormalizedField {
fun fixParents(old: ExecutableNormalizedField?, new: ExecutableNormalizedField?) {
if (old == null || new == null || new.parent == null) {
return
}
val newParent = new.parent.toBuilder()
.children(old.parent.children.filter { it !== old } + new)
.build()
new.replaceParent(newParent)
// Do recursively for all ancestors
fixParents(old = old.parent, new = newParent)
}
children.forEach {
it.replaceParent(this)
}
return toBuilder()
.children(children)
.build()
.also {
fixParents(old = this, new = it)
}
}
val ExecutableNormalizedField.queryPath: NadelQueryPath get() = NadelQueryPath(listOfResultKeys)
inline fun Document.getDefinitionsOfType(): List {
return getDefinitionsOfType(T::class.java)
}
fun deepClone(fields: List): List {
return fields.map {
it.toBuilder()
.children(deepClone(fields = it.children))
.build()
}
}
fun GraphQLType.unwrapOne(): GraphQLType {
return GraphQLTypeUtil.unwrapOne(this)
}
fun GraphQLType.unwrapAll(): GraphQLUnmodifiedType {
return GraphQLTypeUtil.unwrapAll(this)
}
fun GraphQLType.unwrapNonNull(): GraphQLType {
return GraphQLTypeUtil.unwrapNonNull(this)
}
val GraphQLType.isList: Boolean get() = GraphQLTypeUtil.isList(this)
val GraphQLType.isNonNull: Boolean get() = GraphQLTypeUtil.isNonNull(this)
val GraphQLType.isWrapped: Boolean get() = GraphQLTypeUtil.isWrapped(this)
val GraphQLType.isNotWrapped: Boolean get() = GraphQLTypeUtil.isNotWrapped(this)
fun AnyAstType.unwrapOne(): AnyAstType {
return TypeUtil.unwrapOne(this)
}
fun AnyAstType.unwrapAll(): TypeName {
return TypeUtil.unwrapAll(this)
}
fun AnyAstType.unwrapNonNull(): AnyAstType {
return if (isNonNull) unwrapOne() else this
}
val AnyAstType.isList: Boolean get() = TypeUtil.isList(this)
val AnyAstType.isNonNull: Boolean get() = TypeUtil.isNonNull(this)
val AnyAstType.isWrapped: Boolean get() = TypeUtil.isWrapped(this)
val AnyAstType.isNotWrapped: Boolean get() = !isWrapped
internal fun mergeResults(results: List): ExecutionResult {
val data: MutableJsonMap = mutableMapOf()
val extensions: MutableJsonMap = mutableMapOf()
val errors: MutableList = mutableListOf()
fun putAndMergeTopLevelData(oneData: JsonMap) {
for ((topLevelFieldName: String, newTopLevelFieldValue: Any?) in oneData) {
if (topLevelFieldName in data) {
val existingValue = data[topLevelFieldName]
if (existingValue == null) {
data[topLevelFieldName] = newTopLevelFieldValue
} else if (existingValue is AnyMap && newTopLevelFieldValue is AnyMap) {
existingValue.asMutableJsonMap().putAll(
newTopLevelFieldValue.asJsonMap(),
)
}
} else {
data[topLevelFieldName] = newTopLevelFieldValue
}
}
}
for (result in results) {
val resultData = result.getData()
if (resultData != null) {
putAndMergeTopLevelData(resultData)
}
errors.addAll(result.errors)
result.extensions?.asJsonMap()?.let(extensions::putAll)
}
return newExecutionResult()
.data(data)
.extensions(extensions.let {
@Suppress("UNCHECKED_CAST") // .extensions should take in a Map<*, *> instead of strictly Map
it as Map
}.takeIf {
it.isNotEmpty()
})
.errors(errors)
.build()
}
fun makeFieldCoordinates(typeName: String, fieldName: String): FieldCoordinates {
return FieldCoordinates.coordinates(typeName, fieldName)
}
fun makeFieldCoordinates(parentType: GraphQLObjectType, field: GraphQLFieldDefinition): FieldCoordinates {
return makeFieldCoordinates(typeName = parentType.name, fieldName = field.name)
}
fun ExecutionIdProvider.provide(executionInput: ExecutionInput): ExecutionId {
return provide(executionInput.query, executionInput.operationName, executionInput.context)
}
fun ServiceExecutionResult.copy(
data: MutableJsonMap = this.data,
errors: MutableList = this.errors,
extensions: MutableJsonMap = this.extensions,
): ServiceExecutionResult {
return newServiceExecutionResult(data, errors, extensions)
}
fun newServiceExecutionResult(
data: MutableJsonMap = mutableMapOf(),
errors: MutableList = mutableListOf(),
extensions: MutableJsonMap = mutableMapOf(),
): ServiceExecutionResult {
return ServiceExecutionResult(data, errors, extensions)
}
fun newServiceExecutionResult(
error: GraphQLError,
): ServiceExecutionResult {
return newServiceExecutionResult(
errors = mutableListOf(
error.toSpecification(),
),
)
}
fun newExecutionResult(
data: Any? = null,
error: GraphQLError,
): ExecutionResultImpl {
return newExecutionResult()
.data(data)
.addError(error)
.build()
}
fun newExecutionErrorResult(
field: ExecutableNormalizedField,
error: GraphQLError,
): ExecutionResultImpl {
return newExecutionResult(
data = mutableMapOf(
field.resultKey to null,
),
error = error,
)
}
fun ExecutableNormalizedField.getOperationKind(
schema: GraphQLSchema,
): OperationDefinition.Operation {
val objectTypeName = objectTypeNames.singleOrNull()
?: error("Top level field can only belong to one operation type")
return when {
schema.queryType.name == objectTypeName -> OperationDefinition.Operation.QUERY
schema.mutationType?.name?.equals(objectTypeName) == true -> OperationDefinition.Operation.MUTATION
schema.subscriptionType?.name?.equals(objectTypeName) == true -> OperationDefinition.Operation.SUBSCRIPTION
else -> error("Type '$objectTypeName' is not one of the standard GraphQL operation types")
}
}
/**
* Standard [Document.getOperationDefinition] does not suit needs as it doesn't handle null
* operation names, and returns a yucky [java.util.Optional].
*
* Don't use [graphql.language.NodeUtil.getOperation] because that does weird things and stores
* operation and fragment definitions in [Map]s.
*/
internal fun Document.getOperationDefinitionOrNull(operationName: String?): OperationDefinition? {
if (operationName == null || operationName.isEmpty()) {
return definitions.singleOfTypeOrNull()
}
return definitions.singleOfTypeOrNull { def ->
def.name == operationName
}
}
internal suspend fun NadelInstrumentation.beginExecute(
query: ExecutableNormalizedOperation,
queryDocument: Document,
executionInput: ExecutionInput,
graphQLSchema: GraphQLSchema,
instrumentationState: InstrumentationState?,
): InstrumentationContext? {
val nadelInstrumentationExecuteOperationParameters = NadelInstrumentationExecuteOperationParameters(
query,
queryDocument,
graphQLSchema,
executionInput.variables,
queryDocument.getOperationDefinitionOrNull(executionInput.operationName)
?: error("Unable to find operation. This should not happen. Query document should be valid by now."),
instrumentationState,
executionInput.context,
)
return beginExecute(nadelInstrumentationExecuteOperationParameters)
.asDeferred()
.await()
}
/**
* Turns GraphQL types to object types when possible e.g. finds concrete implementations
* for interfaces, gets object types inside unions, and returns objects as is.
*/
fun resolveObjectTypes(
schema: GraphQLSchema,
type: GraphQLType,
onNotObjectType: (GraphQLType) -> Nothing,
): List {
return when (val unwrappedType = type.unwrapAll()) {
is GraphQLObjectType -> listOf(unwrappedType)
is GraphQLUnionType -> unwrappedType.types.flatMap {
resolveObjectTypes(schema, type = it, onNotObjectType)
}
is GraphQLInterfaceType -> schema.getImplementations(unwrappedType)
else -> onNotObjectType(unwrappedType)
}
}
/**
* Creates a GraphQLErrorException based on the data of this GraphQLError
*/
fun GraphQLError.toGraphQLErrorException(): GraphqlErrorException {
return GraphqlErrorException.newErrorException()
.message(this.message)
.sourceLocations(this.locations)
.errorClassification(this.errorType)
.path(this.path)
.extensions(this.extensions)
.build()
}
val AnyAstNode.isExtensionDef: Boolean
get() {
return this is ObjectTypeExtensionDefinition
|| this is InterfaceTypeExtensionDefinition
|| this is EnumTypeExtensionDefinition
|| this is ScalarTypeExtensionDefinition
|| this is InputObjectTypeExtensionDefinition
|| this is SchemaExtensionDefinition
|| this is UnionTypeExtensionDefinition
}
fun makeNormalizedInputValue(
type: GraphQLInputType,
value: AnyAstValue,
): NormalizedInputValue {
return NormalizedInputValue(
GraphQLTypeUtil.simplePrint(type), // type name
value, // value
)
}
/**
* Will return a new NormalizedInputValue with a new typename. The type will be renamed but the
* original wrapping will be intact.
*
* todo: find a better way of doing this? currently it's just a string.replace hack
*/
fun NormalizedInputValue.replaceTypeName(newTypeName: String): NormalizedInputValue {
val newTypenameWithOriginalWrapping = this.typeName.replace(this.unwrappedTypeName, newTypeName)
return NormalizedInputValue(newTypenameWithOriginalWrapping, this.value)
}
internal fun javaValueToAstValue(value: Any?): AnyAstValue {
return when (value) {
is AnyList -> ArrayValue(
value.map(::javaValueToAstValue),
)
is AnyMap -> ObjectValue
.newObjectValue()
.objectFields(
value.asJsonMap().map {
ObjectField(it.key, javaValueToAstValue(it.value))
},
)
.build()
null -> NullValue
.newNullValue()
.build()
is Double -> FloatValue.newFloatValue()
.value(value.toBigDecimal())
.build()
is Float -> FloatValue.newFloatValue()
.value(value.toBigDecimal())
.build()
is Number -> IntValue.newIntValue()
.value(value.toLong().toBigInteger())
.build()
is String -> StringValue.newStringValue()
.value(value)
.build()
is Boolean -> BooleanValue.newBooleanValue()
.value(value)
.build()
else -> error("Unknown value type '${value.javaClass.name}'")
}
}
val GraphQLSchema.operationTypes
get() = listOfNotNull(queryType, mutationType, subscriptionType)