Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Schema name conflict errors #2589

Merged
merged 10 commits into from
Oct 21, 2022
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,8 @@
import { ValidationOptions } from "../validationRules";

export const emptyValidationOptions: ValidationOptions = {
disallowedScalarFieldNames: [],
disallowedEntityFieldNames: [],
disallowedEntityListFieldNames: [],
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 {
scalar?: 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?.scalar)
const disallowedInputParameterNamesRule = ApolloIOSDisallowedInputParameterNames(options.disallowedInputParameterNames)
return [
NoAnonymousQueries,
Expand Down
Loading