Skip to content

Commit a13fafd

Browse files
authored
Refactor schema validation (#6396)
* Refactor schema validation * unbreak IJ plugin * add terms to the glossary
1 parent c01cace commit a13fafd

File tree

37 files changed

+437
-356
lines changed

37 files changed

+437
-356
lines changed

benchmark/microbenchmark/src/main/graphql/conferences/extra.graphqls

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
extend schema
22
@link(
3-
url: "https://specs.apollo.dev/kotlin_labs/v0.3",
3+
url: "https://specs.apollo.dev/kotlin_labs/v0.4",
44
import: ["@fieldPolicy", "@typePolicy"]
55
)
66

design-docs/Glossary.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,3 +125,29 @@ Fields that are used to compute a Cache key for an object.
125125

126126
Field arguments that control pagination, e.g. `first`, `after`, etc. They should be omitted when computing a field key so different pages can be merged into the same field.
127127

128+
129+
### API schema
130+
131+
The server schema as seen from introspection (can be either JSON or SDL)
132+
133+
### Server definition
134+
135+
A definition present in the API schema.
136+
137+
Examples: `@include`, `@oneOf`, `__Schema`
138+
139+
### Client definition
140+
141+
A definition that is added to the API schema by the client, using either @link or by concatenating new definitions to the schema.
142+
143+
### Linked definition
144+
145+
A definition is added to a schema using @link.
146+
147+
Examples: `@targetName`, `@typePolicy`
148+
149+
### Checked definitions
150+
151+
A definition that is recognized by Apollo Kotlin and required to be a certain shape. Apollo Kotlin checks those definitions to avoid crashes in the compiler and/or surprising behaviours. This is typically the case for server definitions that and are not specified yet and/or may vary depending on the version of the spec used.
152+
153+
Examples: `@oneOf`, `@semanticNonNull`, `@targetName`

intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/inspection/GraphQLUnresolvedReferenceInspectionSuppressor.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.apollographql.ijplugin.inspection
22

3+
import com.apollographql.apollo.annotations.ApolloInternal
34
import com.apollographql.apollo.ast.GQLDirectiveDefinition
45
import com.apollographql.apollo.ast.linkDefinitions
56
import com.apollographql.ijplugin.util.KOTLIN_LABS_DEFINITIONS
@@ -15,6 +16,7 @@ import com.intellij.lang.jsgraphql.psi.GraphQLDirective
1516
import com.intellij.lang.jsgraphql.psi.GraphQLDirectivesAware
1617
import com.intellij.psi.PsiElement
1718

19+
@OptIn(ApolloInternal::class)
1820
private val KNOWN_DIRECTIVES: List<GQLDirectiveDefinition> by lazy {
1921
linkDefinitions().directives() + NULLABILITY_DEFINITIONS.directives() + KOTLIN_LABS_DEFINITIONS.directives()
2022
}

intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/util/Link.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
package com.apollographql.ijplugin.util
44

55
import com.apollographql.apollo.annotations.ApolloInternal
6+
import com.apollographql.apollo.ast.AUTO_IMPORTED_KOTLIN_LABS_VERSION
67
import com.apollographql.apollo.ast.GQLDefinition
78
import com.apollographql.apollo.ast.GQLDirectiveDefinition
89
import com.apollographql.apollo.ast.GQLNamed
9-
import com.apollographql.apollo.ast.KOTLIN_LABS_VERSION
1010
import com.apollographql.apollo.ast.NULLABILITY_VERSION
1111
import com.apollographql.apollo.ast.kotlinLabsDefinitions
1212
import com.apollographql.apollo.ast.nullabilityDefinitions
@@ -28,10 +28,10 @@ val NULLABILITY_DEFINITIONS: List<GQLDefinition> by lazy {
2828
nullabilityDefinitions(NULLABILITY_VERSION)
2929
}
3030

31-
const val KOTLIN_LABS_URL = "https://specs.apollo.dev/kotlin_labs/$KOTLIN_LABS_VERSION"
31+
const val KOTLIN_LABS_URL = "https://specs.apollo.dev/kotlin_labs/$AUTO_IMPORTED_KOTLIN_LABS_VERSION"
3232

3333
val KOTLIN_LABS_DEFINITIONS: List<GQLDefinition> by lazy {
34-
kotlinLabsDefinitions(KOTLIN_LABS_VERSION)
34+
kotlinLabsDefinitions(AUTO_IMPORTED_KOTLIN_LABS_VERSION)
3535
}
3636

3737
const val CATCH = "catch"

libraries/apollo-ast/api/apollo-ast.api

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -766,8 +766,6 @@ public final class com/apollographql/apollo/ast/GqldirectiveKt {
766766

767767
public final class com/apollographql/apollo/ast/GqldocumentKt {
768768
public static final fun builtinDefinitions ()Ljava/util/List;
769-
public static final fun kotlinLabsDefinitions (Ljava/lang/String;)Ljava/util/List;
770-
public static final fun linkDefinitions ()Ljava/util/List;
771769
public static final fun nullabilityDefinitions (Ljava/lang/String;)Ljava/util/List;
772770
public static final fun toSchema (Lcom/apollographql/apollo/ast/GQLDocument;)Lcom/apollographql/apollo/ast/Schema;
773771
public static final fun withBuiltinDefinitions (Lcom/apollographql/apollo/ast/GQLDocument;)Lcom/apollographql/apollo/ast/GQLDocument;

libraries/apollo-ast/api/apollo-ast.klib.api

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1298,6 +1298,4 @@ final fun (kotlin/String).com.apollographql.apollo.ast/toGQLValue(com.apollograp
12981298
final fun (okio/Path).com.apollographql.apollo.ast/parseAsGQLDocument(com.apollographql.apollo.ast/ParserOptions = ...): com.apollographql.apollo.ast/GQLResult<com.apollographql.apollo.ast/GQLDocument> // com.apollographql.apollo.ast/parseAsGQLDocument|parseAsGQLDocument@okio.Path(com.apollographql.apollo.ast.ParserOptions){}[0]
12991299
final fun (okio/Path).com.apollographql.apollo.ast/toGQLDocument(com.apollographql.apollo.ast/ParserOptions = ..., kotlin/Boolean = ...): com.apollographql.apollo.ast/GQLDocument // com.apollographql.apollo.ast/toGQLDocument|toGQLDocument@okio.Path(com.apollographql.apollo.ast.ParserOptions;kotlin.Boolean){}[0]
13001300
final fun com.apollographql.apollo.ast/builtinDefinitions(): kotlin.collections/List<com.apollographql.apollo.ast/GQLDefinition> // com.apollographql.apollo.ast/builtinDefinitions|builtinDefinitions(){}[0]
1301-
final fun com.apollographql.apollo.ast/kotlinLabsDefinitions(kotlin/String): kotlin.collections/List<com.apollographql.apollo.ast/GQLDefinition> // com.apollographql.apollo.ast/kotlinLabsDefinitions|kotlinLabsDefinitions(kotlin.String){}[0]
1302-
final fun com.apollographql.apollo.ast/linkDefinitions(): kotlin.collections/List<com.apollographql.apollo.ast/GQLDefinition> // com.apollographql.apollo.ast/linkDefinitions|linkDefinitions(){}[0]
13031301
final fun com.apollographql.apollo.ast/nullabilityDefinitions(kotlin/String): kotlin.collections/List<com.apollographql.apollo.ast/GQLDefinition> // com.apollographql.apollo.ast/nullabilityDefinitions|nullabilityDefinitions(kotlin.String){}[0]

libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo/ast/ForeignSchema.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,7 @@ class ForeignSchema(
1818
val version: String,
1919
val definitions: List<GQLDefinition>,
2020
val directivesToStrip: List<String> = definitions.filterIsInstance<GQLDirective>().map { it.name },
21-
)
21+
) {
22+
val nameWithVersion: String
23+
get() = "$name/$version"
24+
}

libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo/ast/Issue.kt

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package com.apollographql.apollo.ast
22

3-
import com.apollographql.apollo.annotations.ApolloInternal
3+
import com.apollographql.apollo.annotations.ApolloDeprecatedSince
44

55
/**
66
* All the issues that can be collected while analyzing a graphql document
@@ -33,20 +33,17 @@ class ParsingError(override val message: String, override val sourceLocation: So
3333
/**
3434
* An unknown directive was found.
3535
*
36-
* In a perfect world everyone uses SDL schemas, and we can validate directives but in this world, a lot of users rely
37-
* on introspection schemas that do not contain directives. If this happens, we pass them through without validation.
36+
* In case a user rely on non-introspection schemas (that do not contain directives definitions), the apollo compiler:
37+
* - adds the built-in directives (`@include`, `@skip`, ...)
38+
* - adds the `kotlin_labs/v3` directives (for legacy reasons, will be removed in a future version)
3839
*
39-
* In some cases (e.g. `@oneOf`) we want to enforce that the directive is defined. In that case [requireDefinition] is true and the issue
40-
* will be raised as an error rather than warning.
40+
* For anything else, including `@defer` and `@oneOf`, a full schema is required so that the compiler can do feature
41+
* detection and validate operation based on what the server actually supports.
4142
*/
42-
class UnknownDirective @ApolloInternal constructor(
43+
class UnknownDirective(
4344
override val message: String,
4445
override val sourceLocation: SourceLocation?,
45-
@ApolloInternal
46-
val requireDefinition: Boolean,
47-
) : GraphQLValidationIssue {
48-
constructor(message: String, sourceLocation: SourceLocation?) : this(message, sourceLocation, false)
49-
}
46+
) : GraphQLValidationIssue
5047

5148
/**
5249
* The definition is inconsistent with the expected one.
@@ -72,12 +69,16 @@ class UnusedFragment(override val message: String, override val sourceLocation:
7269
*/
7370
class DuplicateTypeName(override val message: String, override val sourceLocation: SourceLocation?) : GraphQLValidationIssue
7471

72+
/**
73+
* This is a bit abused for kotlin_labs directive that override the existing ones for compatibility reasons.
74+
* This is so that `ApolloCompiler` can later on treat them as warnings.
75+
*/
7576
class DirectiveRedefinition(
7677
val name: String,
7778
existingSourceLocation: SourceLocation?,
7879
override val sourceLocation: SourceLocation?,
7980
) : GraphQLValidationIssue {
80-
override val message = "Directive '${name}' is defined multiple times. First definition is: ${existingSourceLocation.pretty()}"
81+
override val message = "Implicit kotlin_labs definition '@${name}' overrides explicit one provided by the schema. Import kotlin_labs explicitly using @link."
8182
}
8283

8384
class NoQueryType(override val message: String, override val sourceLocation: SourceLocation?) : GraphQLValidationIssue

libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo/ast/Schema.kt

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,9 @@ class Schema internal constructor(
5858
)
5959

6060
/**
61-
* @param name the current name of the directive (like "kotlin_labs__nonnull")
61+
* @param name the current name of the directive (like "kotlin_labs__targetName")
6262
*
63-
* @return the original directive name (like "nonnull")
63+
* @return the original directive name (like "targetName") or null if the directive was not linked.
6464
*/
6565
fun originalDirectiveName(name: String): String {
6666
return foreignNames["@$name"]?.substring(1) ?: name
@@ -179,7 +179,7 @@ class Schema internal constructor(
179179
val directives = typeDefinitions.values.filterIsInstance<GQLObjectTypeDefinition>().flatMap { it.directives } +
180180
typeDefinitions.values.filterIsInstance<GQLInterfaceTypeDefinition>().flatMap { it.directives } +
181181
typeDefinitions.values.filterIsInstance<GQLUnionTypeDefinition>().flatMap { it.directives }
182-
return directives.any { it.name == TYPE_POLICY }
182+
return directives.any { originalDirectiveName(it.name) == TYPE_POLICY }
183183
}
184184

185185
/**
@@ -199,15 +199,21 @@ class Schema internal constructor(
199199
}
200200

201201
companion object {
202+
@ApolloExperimental
203+
const val ONE_OF = "oneOf"
204+
@ApolloExperimental
205+
const val DEFER = "defer"
206+
207+
@ApolloExperimental
208+
const val LINK = "link"
209+
202210
const val TYPE_POLICY = "typePolicy"
203211
const val FIELD_POLICY = "fieldPolicy"
204212
const val NONNULL = "nonnull"
205213
const val OPTIONAL = "optional"
206214
const val REQUIRES_OPT_IN = "requiresOptIn"
207215
const val TARGET_NAME = "targetName"
208216

209-
@ApolloExperimental
210-
const val ONE_OF = "oneOf"
211217
@ApolloExperimental
212218
const val CATCH = "catch"
213219
@ApolloExperimental
@@ -216,8 +222,6 @@ class Schema internal constructor(
216222
const val SEMANTIC_NON_NULL = "semanticNonNull"
217223
@ApolloExperimental
218224
const val SEMANTIC_NON_NULL_FIELD = "semanticNonNullField"
219-
@ApolloExperimental
220-
const val LINK = "link"
221225

222226
const val FIELD_POLICY_FOR_FIELD = "forField"
223227
const val FIELD_POLICY_KEY_ARGS = "keyArgs"
@@ -242,14 +246,18 @@ class Schema internal constructor(
242246
}
243247

244248
internal fun rootOperationTypeDefinition(operationType: String, definitions: List<GQLDefinition>): GQLTypeDefinition? {
245-
return definitions.filterIsInstance<GQLSchemaDefinition>().single()
249+
return rootOperationTypeDefinition(definitions.filterIsInstance<GQLSchemaDefinition>().single(), operationType, definitions.filterIsInstance<GQLObjectTypeDefinition>().associateBy { it.name })
250+
}
251+
252+
internal fun rootOperationTypeDefinition(schemaTypeDefinition: GQLSchemaDefinition, operationType: String, typeDefinitions: Map<String, GQLTypeDefinition>): GQLTypeDefinition? {
253+
return schemaTypeDefinition
246254
.rootOperationTypeDefinitions
247255
.singleOrNull {
248256
it.operationType == operationType
249257
}
250258
?.namedType
251259
?.let { namedType ->
252-
definitions.filterIsInstance<GQLObjectTypeDefinition>().single { it.name == namedType }
260+
typeDefinitions.get(namedType)
253261
}
254262
}
255263
}

0 commit comments

Comments
 (0)