-
Notifications
You must be signed in to change notification settings - Fork 2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
A few cases of invalid @key directive arrangements are currently possible with our existing validation suite. This PR adds 3 new validations to ensure that users are informed of these invalid states and block composition appropriately. * Owning type must specify a @key * Extending types can't specify multiple @keys * Extending types must use a @key specified by the owning type Additionally: These invalid arrangements can result in runtime errors when printing CSDL, nor is it really valid to return an "attempted" print of CSDL, so the result of `composedSdl` is now `undefined` in the case that there are composition errors.
- Loading branch information
1 parent
3081569
commit 7b8678f
Showing
7 changed files
with
221 additions
and
1 deletion.
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
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
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
117 changes: 117 additions & 0 deletions
117
...ederation/src/composition/validate/postComposition/__tests__/keysMatchBaseService.test.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,117 @@ | ||
import gql from 'graphql-tag'; | ||
import { composeServices } from '../../../compose'; | ||
import { keysMatchBaseService as validateKeysMatchBaseService } from '../'; | ||
import { graphqlErrorSerializer } from '../../../../snapshotSerializers'; | ||
|
||
expect.addSnapshotSerializer(graphqlErrorSerializer); | ||
|
||
describe('keysMatchBaseService', () => { | ||
it('returns no errors with proper @key usage', () => { | ||
const serviceA = { | ||
typeDefs: gql` | ||
type Product @key(fields: "sku") { | ||
sku: String! | ||
upc: String! | ||
} | ||
`, | ||
name: 'serviceA', | ||
}; | ||
|
||
const serviceB = { | ||
typeDefs: gql` | ||
extend type Product @key(fields: "sku") { | ||
sku: String! @external | ||
price: Int! | ||
} | ||
`, | ||
name: 'serviceB', | ||
}; | ||
|
||
const serviceList = [serviceA, serviceB]; | ||
const { schema, errors } = composeServices(serviceList); | ||
expect(errors).toHaveLength(0); | ||
|
||
const validationErrors = validateKeysMatchBaseService({ | ||
schema, | ||
serviceList, | ||
}); | ||
expect(validationErrors).toHaveLength(0); | ||
}); | ||
|
||
it('requires a @key to be specified on the originating type', () => { | ||
const serviceA = { | ||
typeDefs: gql` | ||
type Product { | ||
sku: String! | ||
upc: String! | ||
} | ||
`, | ||
name: 'serviceA', | ||
}; | ||
|
||
const serviceB = { | ||
typeDefs: gql` | ||
extend type Product @key(fields: "sku") { | ||
sku: String! @external | ||
price: Int! | ||
} | ||
`, | ||
name: 'serviceB', | ||
}; | ||
|
||
const serviceList = [serviceA, serviceB]; | ||
const { schema, errors } = composeServices(serviceList); | ||
expect(errors).toHaveLength(0); | ||
|
||
const validationErrors = validateKeysMatchBaseService({ | ||
schema, | ||
serviceList, | ||
}); | ||
expect(validationErrors).toHaveLength(1); | ||
expect(validationErrors[0]).toMatchInlineSnapshot(` | ||
Object { | ||
"code": "KEY_MISSING_ON_BASE", | ||
"message": "[serviceA] Product -> appears to be an entity but no @key directives are specified on the originating type.", | ||
} | ||
`); | ||
}); | ||
|
||
it('requires extending services to use a @key specified by the originating type', () => { | ||
const serviceA = { | ||
typeDefs: gql` | ||
type Product @key(fields: "sku upc") { | ||
sku: String! | ||
upc: String! | ||
} | ||
`, | ||
name: 'serviceA', | ||
}; | ||
|
||
const serviceB = { | ||
typeDefs: gql` | ||
extend type Product @key(fields: "sku") { | ||
sku: String! @external | ||
price: Int! | ||
} | ||
`, | ||
name: 'serviceB', | ||
}; | ||
|
||
const serviceList = [serviceA, serviceB]; | ||
const { schema, errors } = composeServices(serviceList); | ||
expect(errors).toHaveLength(0); | ||
|
||
const validationErrors = validateKeysMatchBaseService({ | ||
schema, | ||
serviceList, | ||
}); | ||
expect(validationErrors).toHaveLength(1); | ||
expect(validationErrors[0]).toMatchInlineSnapshot(` | ||
Object { | ||
"code": "KEY_NOT_SPECIFIED", | ||
"message": "[serviceB] Product -> extends from serviceA but specifies an invalid @key directive. Valid @key directives are specified by the originating type. Available @key directives for this type are: | ||
@key(fields: \\"sku upc\\")", | ||
} | ||
`); | ||
}); | ||
}); |
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
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
86 changes: 86 additions & 0 deletions
86
packages/apollo-federation/src/composition/validate/postComposition/keysMatchBaseService.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,86 @@ | ||
import { isObjectType, GraphQLError, SelectionNode } from 'graphql'; | ||
import { | ||
logServiceAndType, | ||
errorWithCode, | ||
getFederationMetadata, | ||
} from '../../utils'; | ||
import { PostCompositionValidator } from '.'; | ||
import { printWithReducedWhitespace } from '../../../service'; | ||
|
||
/** | ||
* 1. KEY_MISSING_ON_BASE - Originating types must specify at least 1 @key directive | ||
* 2. MULTIPLE_KEYS_ON_EXTENSION - Extending services may not use more than 1 @key directive | ||
* 3. KEY_NOT_SPECIFIED - Extending services must use a valid @key specified by the originating type | ||
*/ | ||
export const keysMatchBaseService: PostCompositionValidator = function ({ | ||
schema, | ||
}) { | ||
const errors: GraphQLError[] = []; | ||
const types = schema.getTypeMap(); | ||
for (const [parentTypeName, parentType] of Object.entries(types)) { | ||
// Only object types have fields | ||
if (!isObjectType(parentType)) continue; | ||
|
||
const typeFederationMetadata = getFederationMetadata(parentType); | ||
|
||
if (typeFederationMetadata) { | ||
const { serviceName, keys } = typeFederationMetadata; | ||
|
||
if (serviceName && keys) { | ||
if (!keys[serviceName]) { | ||
errors.push( | ||
errorWithCode( | ||
'KEY_MISSING_ON_BASE', | ||
logServiceAndType(serviceName, parentTypeName) + | ||
`appears to be an entity but no @key directives are specified on the originating type.`, | ||
), | ||
); | ||
continue; | ||
} | ||
|
||
const availableKeys = keys[serviceName].map(printFieldSet); | ||
Object.entries(keys) | ||
// No need to validate that the owning service matches its specified keys | ||
.filter(([service]) => service !== serviceName) | ||
.forEach(([extendingService, keyFields]) => { | ||
// Extensions can't specify more than one key | ||
if (keyFields.length > 1) { | ||
errors.push( | ||
errorWithCode( | ||
'MULTIPLE_KEYS_ON_EXTENSION', | ||
logServiceAndType(extendingService, parentTypeName) + | ||
`is extended from service ${serviceName} but specifies multiple @key directives. Extensions may only specify one @key.`, | ||
), | ||
); | ||
return; | ||
} | ||
|
||
// This isn't representative of an invalid graph, but it is an existing | ||
// limitation of the query planner that we want to validate against for now. | ||
// In the future, `@key`s just need to be "reachable" through a number of | ||
// services which can link one key to another via "joins". | ||
const extensionKey = printFieldSet(keyFields[0]); | ||
if (!availableKeys.includes(extensionKey)) { | ||
errors.push( | ||
errorWithCode( | ||
'KEY_NOT_SPECIFIED', | ||
logServiceAndType(extendingService, parentTypeName) + | ||
`extends from ${serviceName} but specifies an invalid @key directive. Valid @key directives are specified by the originating type. Available @key directives for this type are:\n` + | ||
`\t${availableKeys | ||
.map((fieldSet) => `@key(fields: "${fieldSet}")`) | ||
.join('\n\t')}`, | ||
), | ||
); | ||
return; | ||
} | ||
}); | ||
} | ||
} | ||
} | ||
|
||
return errors; | ||
}; | ||
|
||
function printFieldSet(selections: readonly SelectionNode[]): string { | ||
return selections.map(printWithReducedWhitespace).join(' '); | ||
} |