Skip to content

Commit

Permalink
fix: Schema name conflict errors (#2589)
Browse files Browse the repository at this point in the history
Co-authored-by: Anthony Miller <[email protected]>
  • Loading branch information
calvincestari and AnthonyMDev authored Oct 21, 2022
1 parent 2a4c1c6 commit 14075d5
Show file tree
Hide file tree
Showing 20 changed files with 715 additions and 111 deletions.
31 changes: 27 additions & 4 deletions Sources/ApolloCodegenLib/ApolloCodegen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public class ApolloCodegen {
case graphQLSourceValidationFailure(atLines: [String])
case testMocksInvalidSwiftPackageConfiguration
case inputSearchPathInvalid(path: String)
case schemaNameConflict(name: String)

public var errorDescription: String? {
switch self {
Expand All @@ -24,6 +25,8 @@ public class ApolloCodegen {
return "Schema Types must be generated with module type 'swiftPackageManager' to generate a swift package for test mocks."
case let .inputSearchPathInvalid(path):
return "Input search path '\(path)' is invalid. Input search paths must include a file extension component. (eg. '.graphql')"
case let .schemaNameConflict(name):
return "Schema name \(name) conflicts with name of a type in your GraphQL schema. Please choose a different schema name. Suggestions: \(name)Schema, \(name)GraphQL, \(name)API"
}
}
}
Expand All @@ -47,16 +50,18 @@ public class ApolloCodegen {
rootURL: URL? = nil,
fileManager: ApolloFileManager = .default
) throws {

let configContext = ConfigurationContext(
config: configuration,
rootURL: rootURL
)

let compilationResult = try compileGraphQLResult(
configContext,
experimentalFeatures: configuration.experimentalFeatures
)

try validate(config: configContext)
try validate(config: configContext, compilationResult: compilationResult)

let ir = IR(
schemaName: configContext.schemaName,
Expand Down Expand Up @@ -107,7 +112,7 @@ public class ApolloCodegen {
}

/// Performs validation against deterministic errors that will cause code generation to fail.
static func validate(config: ConfigurationContext) throws {
static func validate(config: ConfigurationContext, compilationResult: CompilationResult) throws {
if case .swiftPackage = config.output.testMocks,
config.output.schemaTypes.moduleType != .swiftPackageManager {
throw Error.testMocksInvalidSwiftPackageConfiguration
Expand All @@ -119,6 +124,8 @@ public class ApolloCodegen {
for searchPath in config.input.operationSearchPaths {
try validate(inputSearchPath: searchPath)
}

try validate(schemaName: config.schemaName, compilationResult: compilationResult)
}

static private func validate(inputSearchPath: String) throws {
Expand All @@ -127,6 +134,19 @@ public class ApolloCodegen {
}
}

static private func validate(schemaName: String, compilationResult: CompilationResult) throws {
guard
!compilationResult.referencedTypes.contains(where: { namedType in
namedType.swiftName == schemaName.firstUppercased
}),
!compilationResult.fragments.contains(where: { fragmentDefinition in
fragmentDefinition.name == schemaName.firstUppercased
})
else {
throw Error.schemaNameConflict(name: schemaName)
}
}

/// Performs GraphQL source validation and compiles the schema and operation source documents.
static func compileGraphQLResult(
_ config: ConfigurationContext,
Expand All @@ -137,10 +157,12 @@ public class ApolloCodegen {
let graphQLSchema = try createSchema(config, frontend)
let operationsDocument = try createOperationsDocument(config, frontend, experimentalFeatures)

let validationOptions = ValidationOptions(config: config)

let graphqlErrors = try frontend.validateDocument(
schema: graphQLSchema,
document: operationsDocument,
options: ValidationOptions(config: config.config)
validationOptions: validationOptions
)

guard graphqlErrors.isEmpty else {
Expand All @@ -152,7 +174,8 @@ public class ApolloCodegen {
return try frontend.compile(
schema: graphQLSchema,
document: operationsDocument,
experimentalLegacySafelistingCompatibleOperations: experimentalFeatures.legacySafelistingCompatibleOperations
experimentalLegacySafelistingCompatibleOperations: experimentalFeatures.legacySafelistingCompatibleOperations,
validationOptions: validationOptions
)
}

Expand Down

Large diffs are not rendered by default.

12 changes: 8 additions & 4 deletions Sources/ApolloCodegenLib/Frontend/GraphQLJSFrontend.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,25 +88,29 @@ public final class GraphQLJSFrontend {
public func validateDocument(
schema: GraphQLSchema,
document: GraphQLDocument,
options: ValidationOptions
validationOptions: ValidationOptions
) throws -> [GraphQLError] {
return try library.call(
"validateDocument",
with: schema,
document,
ValidationOptions.Bridged(from: options, bridge: self.bridge)
ValidationOptions.Bridged(from: validationOptions, bridge: self.bridge)
)
}

/// Compiles a GraphQL document into an intermediate representation that is more suitable for analysis and code generation.
public func compile(
schema: GraphQLSchema,
document: GraphQLDocument,
experimentalLegacySafelistingCompatibleOperations: Bool = false
experimentalLegacySafelistingCompatibleOperations: Bool = false,
validationOptions: ValidationOptions
) throws -> CompilationResult {
return try library.call(
"compileDocument",
with: schema, document, experimentalLegacySafelistingCompatibleOperations
with: schema,
document,
experimentalLegacySafelistingCompatibleOperations,
ValidationOptions.Bridged(from: validationOptions, bridge: self.bridge)
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { DisallowedFieldNames, ValidationOptions } from "../validationRules";

const disallowedFieldNames: DisallowedFieldNames = {
allFields: [],
entity: [],
entityList: []
}

export const emptyValidationOptions: ValidationOptions = {
schemaName: "TestSchema",
disallowedFieldNames: disallowedFieldNames,
disallowedInputParameterNames: [],
};
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
DocumentNode,
GraphQLEnumType,
} from "graphql";
import { emptyValidationOptions } from "../__testUtils__/validationHelpers";

describe("given schema", () => {
const schemaSDL: string = `
Expand Down Expand Up @@ -49,7 +50,7 @@ describe("given schema", () => {
);

it("should compile enum values with deprecation reason", () => {
const compilationResult: CompilationResult = compileDocument(schema, document, false);
const compilationResult: CompilationResult = compileDocument(schema, document, false, emptyValidationOptions);
const speciesEnum: GraphQLEnumType = compilationResult.referencedTypes.find(function(element) {
return element.name == 'Species'
}) as GraphQLEnumType
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
GraphQLSchema,
DocumentNode
} from "graphql";
import { emptyValidationOptions } from "../__testUtils__/validationHelpers";

describe("given schema", () => {
const schemaSDL: string = `
Expand Down Expand Up @@ -48,7 +49,7 @@ describe("given schema", () => {
);

it("should compile inline fragment with inclusion condition", () => {
const compilationResult: CompilationResult = compileDocument(schema, document, false);
const compilationResult: CompilationResult = compileDocument(schema, document, false, emptyValidationOptions);
const operation = compilationResult.operations[0];
const allAnimals = operation.selectionSet.selections[0] as Field;
const inlineFragment = allAnimals?.selectionSet?.selections?.[0] as InlineFragment;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
import {
join
} from 'path';
import { emptyValidationOptions } from "../__testUtils__/validationHelpers";

describe("mutation defined using ReportCarProblemInput", () => {
const documentString: string = `
Expand All @@ -39,7 +40,7 @@ describe("mutation defined using ReportCarProblemInput", () => {
const schema: GraphQLSchema = loadSchemaFromSources([new Source(schemaJSON, "TestSchema.json", { line: 1, column: 1 })]);

it("should compile with referencedTypes including ReportCarProblemInput and CarProblem enum", () => {
const compilationResult: CompilationResult = compileDocument(schema, document, false);
const compilationResult: CompilationResult = compileDocument(schema, document, false, emptyValidationOptions);
const reportCarProblemInput: GraphQLInputObjectType = compilationResult.referencedTypes.find(function(element) {
return element.name == 'ReportCarProblemInput'
}) as GraphQLInputObjectType
Expand Down Expand Up @@ -78,7 +79,7 @@ describe("mutation defined using ReportCarProblemInput", () => {
const schema: GraphQLSchema = loadSchemaFromSources([new Source(schemaSDL, "Test Schema", { line: 1, column: 1 })]);

it("should compile with referencedTypes inlcuding InputObject and enum", () => {
const compilationResult: CompilationResult = compileDocument(schema, document, false);
const compilationResult: CompilationResult = compileDocument(schema, document, false, emptyValidationOptions);
const reportCarProblemInput: GraphQLInputObjectType = compilationResult.referencedTypes.find(function(element) {
return element.name == 'ReportCarProblemInput'
}) as GraphQLInputObjectType
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
GraphQLSchema,
DocumentNode
} from "graphql";
import { emptyValidationOptions } from "../__testUtils__/validationHelpers";

describe("given schema", () => {
const schemaSDL: string =
Expand Down Expand Up @@ -47,7 +48,7 @@ interface Pet {
);

describe("compile document", () => {
const compilationResult: CompilationResult = compileDocument(schema, document, false);
const compilationResult: CompilationResult = compileDocument(schema, document, false, emptyValidationOptions);

it("operation definition should have source including __typename field.", () => {
const operation = compilationResult.operations[0];
Expand All @@ -68,7 +69,7 @@ interface Pet {
});

describe("compile document for legacy compatible safelisting", () => {
const compilationResult: CompilationResult = compileDocument(schema, document, true);
const compilationResult: CompilationResult = compileDocument(schema, document, true, emptyValidationOptions);

it("operation definition should have source including __typename field in each selection set.", () => {
const operation = compilationResult.operations[0];
Expand Down Expand Up @@ -109,7 +110,7 @@ interface Pet {
);

it("operation definition should have source including __typename field with no directives.", () => {
const compilationResult: CompilationResult = compileDocument(schema, document, false);
const compilationResult: CompilationResult = compileDocument(schema, document, false, emptyValidationOptions);
const operation = compilationResult.operations[0];

const expected: string =
Expand Down Expand Up @@ -144,7 +145,7 @@ interface Pet {
);

it("operation definition should have source not including local cache mutation directive.", () => {
const compilationResult: CompilationResult = compileDocument(schema, document, false);
const compilationResult: CompilationResult = compileDocument(schema, document, false, emptyValidationOptions);
const operation = compilationResult.operations[0];

const expected: string =
Expand Down Expand Up @@ -174,7 +175,7 @@ interface Pet {
);

it("fragment definition should have source including __typename field.", () => {
const compilationResult: CompilationResult = compileDocument(schema, document, false);
const compilationResult: CompilationResult = compileDocument(schema, document, false, emptyValidationOptions);
const fragment = compilationResult.fragments[0];

const expected: string =
Expand Down
29 changes: 27 additions & 2 deletions Sources/ApolloCodegenLib/Frontend/JavaScript/src/compiler/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,15 @@ import {
SelectionSetNode,
typeFromAST,
isObjectType,
isInterfaceType
isInterfaceType,
isListType,
isNonNullType,
FieldNode,
} from "graphql";
import * as ir from "./ir";
import { valueFromValueNode } from "./values";
import { applyRequiredStatus } from "graphql/utilities/applyRequiredStatus";
import { ValidationOptions } from "../validationRules";

function filePathForNode(node: ASTNode): string | undefined {
return node.loc?.source?.name;
Expand All @@ -51,7 +55,8 @@ export interface CompilationResult {
export function compileToIR(
schema: GraphQLSchema,
document: DocumentNode,
legacySafelistingCompatibleOperations: boolean
legacySafelistingCompatibleOperations: boolean,
validationOptions: ValidationOptions
): CompilationResult {
// Collect fragment definition nodes upfront so we can compile these as we encounter them.
const fragmentNodeMap = new Map<String, FragmentDefinitionNode>();
Expand Down Expand Up @@ -292,6 +297,26 @@ export function compileToIR(
directives: directives,
};

function validateFieldName(node: FieldNode, disallowedNames?: Array<string>, schemaName?: string) {
if (disallowedNames && schemaName) {
const responseKey = (node.alias ?? node.name).value
const responseKeyFirstLowercase = responseKey.charAt(0).toLowerCase() + responseKey.slice(1)

if (disallowedNames?.includes(responseKeyFirstLowercase)) {
throw new GraphQLError(
`Schema name "${schemaName}" conflicts with name of a generated object API. Please choose a different schema name. Suggestions: "${schemaName}Schema", "${schemaName}GraphQL", "${schemaName}API"`,
{ nodes: node }
);
}
}
}

if (isListType(fieldType) || (isNonNullType(fieldType) && isListType(fieldType.ofType))) {
validateFieldName(selectionNode, validationOptions.disallowedFieldNames?.entityList, validationOptions.schemaName)
} else if (isCompositeType(unwrappedFieldType)) {
validateFieldName(selectionNode, validationOptions.disallowedFieldNames?.entity, validationOptions.schemaName)
}

if (isCompositeType(unwrappedFieldType)) {
const selectionSetNode = selectionNode.selectionSet;

Expand Down
9 changes: 5 additions & 4 deletions Sources/ApolloCodegenLib/Frontend/JavaScript/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,15 +92,16 @@ export function mergeDocuments(documents: DocumentNode[]): DocumentNode {
export function validateDocument(
schema: GraphQLSchema,
document: DocumentNode,
options: ValidationOptions,
validationOptions: ValidationOptions,
): readonly GraphQLError[] {
return validate(schema, document, defaultValidationRules(options));
return validate(schema, document, defaultValidationRules(validationOptions));
}

export function compileDocument(
schema: GraphQLSchema,
document: DocumentNode,
legacySafelistingCompatibleOperations: boolean
legacySafelistingCompatibleOperations: boolean,
validationOptions: ValidationOptions
): CompilationResult {
return compileToIR(schema, document, legacySafelistingCompatibleOperations);
return compileToIR(schema, document, legacySafelistingCompatibleOperations, validationOptions);
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,20 @@ import {

const specifiedRulesToBeRemoved: [ValidationRule] = [NoUnusedFragmentsRule];

export interface DisallowedFieldNames {
allFields?: Array<string>
entity?: Array<string>
entityList?: Array<string>
}

export interface ValidationOptions {
disallowedFieldNames?: Array<string>
schemaName?: string
disallowedFieldNames?: DisallowedFieldNames
disallowedInputParameterNames?: Array<string>
}

export function defaultValidationRules(options: ValidationOptions): ValidationRule[] {
const disallowedFieldNamesRule = ApolloIOSDisallowedFieldNames(options.disallowedFieldNames)
const disallowedFieldNamesRule = ApolloIOSDisallowedFieldNames(options.disallowedFieldNames?.allFields)
const disallowedInputParameterNamesRule = ApolloIOSDisallowedInputParameterNames(options.disallowedInputParameterNames)
return [
NoAnonymousQueries,
Expand Down
Loading

0 comments on commit 14075d5

Please sign in to comment.