Skip to content

Commit

Permalink
Merge pull request #2868 from apollographql/next
Browse files Browse the repository at this point in the history
Update `main` with `next`
  • Loading branch information
sachindshinde authored Nov 27, 2023
2 parents 7903a29 + b6bcfd8 commit 6d700a7
Show file tree
Hide file tree
Showing 43 changed files with 798 additions and 206 deletions.
39 changes: 39 additions & 0 deletions composition-js/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,44 @@
# CHANGELOG for `@apollo/composition`

## 2.6.0
### Minor Changes


- Update `license` field in `package.json` to use `Elastic-2.0` SPDX identifier ([#2741](https://github.com/apollographql/federation/pull/2741))


- Introduce the new `@policy` scope for composition ([#2818](https://github.com/apollographql/federation/pull/2818))

> Note that this directive will only be _fully_ supported by the Apollo Router as a GraphOS Enterprise feature at runtime. Also note that _composition_ of valid `@policy` directive applications will succeed, but the resulting supergraph will not be _executable_ by the Gateway or an Apollo Router which doesn't have the GraphOS Enterprise entitlement.
Users may now compose `@policy` applications from their subgraphs into a supergraph.

The directive is defined as follows:

```graphql
scalar federation__Policy

directive @policy(policies: [[federation__Policy!]!]!) on
| FIELD_DEFINITION
| OBJECT
| INTERFACE
| SCALAR
| ENUM
```

The `Policy` scalar is effectively a `String`, similar to the `FieldSet` type.

In order to compose your `@policy` usages, you must update your subgraph's federation spec version to v2.6 and add the `@policy` import to your existing imports like so:
```graphql
@link(url: "https://specs.apollo.dev/federation/v2.6", import: [..., "@policy"])
```

### Patch Changes

- Updated dependencies [[`b18841be`](https://github.com/apollographql/federation/commit/b18841be897e6d4f47454568776f199e2adb60ae), [`e325b499`](https://github.com/apollographql/federation/commit/e325b499d592dabe61c93112c292c92ca10afbc5)]:
- @apollo/query-graphs@2.6.0
- @apollo/federation-internals@2.6.0

## 2.5.7
### Patch Changes

Expand Down
8 changes: 4 additions & 4 deletions composition-js/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@apollo/composition",
"version": "2.5.7",
"version": "2.6.0",
"description": "Apollo Federation composition utilities",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand All @@ -19,16 +19,16 @@
"composition"
],
"author": "Apollo <packages@apollographql.com>",
"license": "SEE LICENSE IN ./LICENSE",
"license": "Elastic-2.0",
"engines": {
"node": ">=14.15.0"
},
"publishConfig": {
"access": "public"
},
"dependencies": {
"@apollo/federation-internals": "2.5.7",
"@apollo/query-graphs": "2.5.7"
"@apollo/federation-internals": "2.6.0",
"@apollo/query-graphs": "2.6.0"
},
"peerDependencies": {
"graphql": "^16.5.0"
Expand Down
102 changes: 53 additions & 49 deletions composition-js/src/__tests__/compose.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import {
printDirectiveDefinition,
printSchema,
printType,
RequiresScopesSpecDefinition,
} from '@apollo/federation-internals';
import { CompositionOptions, CompositionResult, composeServices } from '../compose';
import gql from 'graphql-tag';
Expand Down Expand Up @@ -4243,15 +4242,20 @@ describe('composition', () => {
});
});

describe('@requiresScopes', () => {
it('comprehensive locations', () => {
// @requiresScopes and @policy behave exactly the same way, and so all tests should be equally applicable to both directives
describe('@requiresScopes and @policy', () => {
const testsToRun = [
{ directiveName: '@requiresScopes', argName: 'scopes', argType: 'requiresScopes__Scope', fedType: 'federation__Scope', identity: 'https://specs.apollo.dev/requiresScopes' },
{ directiveName: '@policy', argName: 'policies', argType: 'policy__Policy', fedType: 'federation__Policy', identity: 'https://specs.apollo.dev/policy' },
]
it.each(testsToRun)('comprehensive locations', ({ directiveName, argName }) => {
const onObject = {
typeDefs: gql`
type Query {
object: ScopedObject!
}
type ScopedObject @requiresScopes(scopes: ["object"]) {
type ScopedObject ${directiveName}(${argName}: ["object"]) {
field: Int!
}
`,
Expand All @@ -4264,7 +4268,7 @@ describe('composition', () => {
interface: ScopedInterface!
}
interface ScopedInterface @requiresScopes(scopes: ["interface"]) {
interface ScopedInterface ${directiveName}(${argName}: ["interface"]) {
field: Int!
}
`,
Expand All @@ -4276,7 +4280,7 @@ describe('composition', () => {
type ScopedInterfaceObject
@interfaceObject
@key(fields: "id")
@requiresScopes(scopes: ["interfaceObject"])
${directiveName}(${argName}: ["interfaceObject"])
{
id: String!
}
Expand All @@ -4286,11 +4290,11 @@ describe('composition', () => {

const onScalar = {
typeDefs: gql`
scalar ScopedScalar @requiresScopes(scopes: ["scalar"])
scalar ScopedScalar ${directiveName}(${argName}: ["scalar"])
# This needs to exist in at least one other subgraph from where it's defined
# as an @interfaceObject (so arbitrarily adding it here). We don't actually
# apply @requiresScopes to this one since we want to see it propagate even
# apply ${directiveName} to this one since we want to see it propagate even
# when it's not applied in all locations.
interface ScopedInterfaceObject @key(fields: "id") {
id: String!
Expand All @@ -4301,7 +4305,7 @@ describe('composition', () => {

const onEnum = {
typeDefs: gql`
enum ScopedEnum @requiresScopes(scopes: ["enum"]) {
enum ScopedEnum ${directiveName}(${argName}: ["enum"]) {
A
B
}
Expand All @@ -4312,7 +4316,7 @@ describe('composition', () => {
const onRootField = {
typeDefs: gql`
type Query {
scopedRootField: Int! @requiresScopes(scopes: ["rootField"])
scopedRootField: Int! ${directiveName}(${argName}: ["rootField"])
}
`,
name: 'on-root-field',
Expand All @@ -4325,7 +4329,7 @@ describe('composition', () => {
}
type ObjectWithScopedField {
field: Int! @requiresScopes(scopes: ["objectField"])
field: Int! ${directiveName}(${argName}: ["objectField"])
}
`,
name: 'on-object-field',
Expand All @@ -4339,7 +4343,7 @@ describe('composition', () => {
type EntityWithScopedField @key(fields: "id") {
id: ID!
field: Int! @requiresScopes(scopes: ["entityField"])
field: Int! ${directiveName}(${argName}: ["entityField"])
}
`,
name: 'on-entity-field',
Expand Down Expand Up @@ -4372,18 +4376,18 @@ describe('composition', () => {
expect(
result.schema
.elementByCoordinate(element)
?.hasAppliedDirective("requiresScopes")
?.hasAppliedDirective(directiveName.slice(1))
).toBeTruthy();
}
});

it('applies @requiresScopes on types as long as it is used once', () => {
it.each(testsToRun)('applies directive on types as long as it is used once', ({ directiveName, argName }) => {
const a1 = {
typeDefs: gql`
type Query {
a: A
}
type A @key(fields: "id") @requiresScopes(scopes: ["a"]) {
type A @key(fields: "id") ${directiveName}(${argName}: ["a"]) {
id: String!
a1: String
}
Expand All @@ -4407,18 +4411,18 @@ describe('composition', () => {
assertCompositionSuccess(result1);
assertCompositionSuccess(result2);

expect(result1.schema.type('A')?.hasAppliedDirective('requiresScopes')).toBeTruthy();
expect(result2.schema.type('A')?.hasAppliedDirective('requiresScopes')).toBeTruthy();
expect(result1.schema.type('A')?.hasAppliedDirective(directiveName.slice(1))).toBeTruthy();
expect(result2.schema.type('A')?.hasAppliedDirective(directiveName.slice(1))).toBeTruthy();
});

it('merges @requiresScopes lists (simple union)', () => {
it.each(testsToRun)('merges ${directiveName} lists (simple union)', ({ directiveName, argName }) => {
const a1 = {
typeDefs: gql`
type Query {
a: A
}
type A @requiresScopes(scopes: ["a"]) @key(fields: "id") {
type A ${directiveName}(${argName}: ["a"]) @key(fields: "id") {
id: String!
a1: String
}
Expand All @@ -4427,7 +4431,7 @@ describe('composition', () => {
};
const a2 = {
typeDefs: gql`
type A @requiresScopes(scopes: ["b"]) @key(fields: "id") {
type A ${directiveName}(${argName}: ["b"]) @key(fields: "id") {
id: String!
a2: String
}
Expand All @@ -4439,19 +4443,19 @@ describe('composition', () => {
assertCompositionSuccess(result);
expect(
result.schema.type('A')
?.appliedDirectivesOf('requiresScopes')
?.[0]?.arguments()?.scopes).toStrictEqual(['a', 'b']
?.appliedDirectivesOf(directiveName.slice(1))
?.[0]?.arguments()?.[argName]).toStrictEqual(['a', 'b']
);
});

it('merges @requiresScopes lists (deduplicates intersecting scopes)', () => {
it.each(testsToRun)('merges ${directiveName} lists (deduplicates intersecting scopes)', ({ directiveName, argName }) => {
const a1 = {
typeDefs: gql`
type Query {
a: A
}
type A @requiresScopes(scopes: ["a", "b"]) @key(fields: "id") {
type A ${directiveName}(${argName}: ["a", "b"]) @key(fields: "id") {
id: String!
a1: String
}
Expand All @@ -4460,7 +4464,7 @@ describe('composition', () => {
};
const a2 = {
typeDefs: gql`
type A @requiresScopes(scopes: ["b", "c"]) @key(fields: "id") {
type A ${directiveName}(${argName}: ["b", "c"]) @key(fields: "id") {
id: String!
a2: String
}
Expand All @@ -4472,37 +4476,37 @@ describe('composition', () => {
assertCompositionSuccess(result);
expect(
result.schema.type('A')
?.appliedDirectivesOf('requiresScopes')
?.[0]?.arguments()?.scopes).toStrictEqual(['a', 'b', 'c']
?.appliedDirectivesOf(directiveName.slice(1))
?.[0]?.arguments()?.[argName]).toStrictEqual(['a', 'b', 'c']
);
});

it('@requiresScopes has correct definition in the supergraph', () => {
it.each(testsToRun)('${directiveName} has correct definition in the supergraph', ({ directiveName, argName, argType, identity }) => {
const a = {
typeDefs: gql`
type Query {
x: Int @requiresScopes(scopes: ["a", "b"])
x: Int ${directiveName}(${argName}: ["a", "b"])
}
`,
name: 'a',
};

const result = composeAsFed2Subgraphs([a]);
assertCompositionSuccess(result);
expect(result.schema.coreFeatures?.getByIdentity(RequiresScopesSpecDefinition.identity)?.url.toString()).toBe(
"https://specs.apollo.dev/requiresScopes/v0.1"
expect(result.schema.coreFeatures?.getByIdentity(identity)?.url.toString()).toBe(
`https://specs.apollo.dev/${directiveName.slice(1)}/v0.1`
);
expect(printDirectiveDefinition(result.schema.directive('requiresScopes')!)).toMatchString(`
directive @requiresScopes(scopes: [[requiresScopes__Scope!]!]!) on FIELD_DEFINITION | OBJECT | INTERFACE | SCALAR | ENUM
expect(printDirectiveDefinition(result.schema.directive(directiveName.slice(1))!)).toMatchString(`
directive ${directiveName}(${argName}: [[${argType}!]!]!) on FIELD_DEFINITION | OBJECT | INTERFACE | SCALAR | ENUM
`);
});

it('composes with existing `Scope` scalar definitions in subgraphs', () => {
it.each(testsToRun)('composes with existing `Scope` scalar definitions in subgraphs', ({ directiveName, argName }) => {
const a = {
typeDefs: gql`
scalar Scope
type Query {
x: Int @requiresScopes(scopes: ["a", "b"])
x: Int ${directiveName}(${argName}: ["a", "b"])
}
`,
name: 'a',
Expand All @@ -4512,7 +4516,7 @@ describe('composition', () => {
typeDefs: gql`
scalar Scope @specifiedBy(url: "not-the-apollo-spec")
type Query {
y: Int @requiresScopes(scopes: ["a", "b"])
y: Int ${directiveName}(${argName}: ["a", "b"])
}
`,
name: 'b',
Expand All @@ -4523,69 +4527,69 @@ describe('composition', () => {
});

describe('validation errors', () => {
it('on incompatible directive location', () => {
it.each(testsToRun)('on incompatible directive location', ({ directiveName, argName, fedType }) => {
const invalidDefinition = {
typeDefs: gql`
scalar federation__Scope
directive @requiresScopes(scopes: [[federation__Scope!]!]!) on ENUM_VALUE
scalar ${fedType}
directive ${directiveName}(${argName}: [[${fedType}!]!]!) on ENUM_VALUE
type Query {
a: Int
}
enum E {
A @requiresScopes(scopes: [])
A ${directiveName}(${argName}: [])
}
`,
name: 'invalidDefinition',
};
const result = composeAsFed2Subgraphs([invalidDefinition]);
expect(errors(result)[0]).toEqual([
"DIRECTIVE_DEFINITION_INVALID",
"[invalidDefinition] Invalid definition for directive \"@requiresScopes\": \"@requiresScopes\" should have locations FIELD_DEFINITION, OBJECT, INTERFACE, SCALAR, ENUM, but found (non-subset) ENUM_VALUE",
`[invalidDefinition] Invalid definition for directive \"${directiveName}\": \"${directiveName}\" should have locations FIELD_DEFINITION, OBJECT, INTERFACE, SCALAR, ENUM, but found (non-subset) ENUM_VALUE`,
]);
});

it('on incompatible args', () => {
it.each(testsToRun)('on incompatible args', ({ directiveName, argName, fedType }) => {
const invalidDefinition = {
typeDefs: gql`
scalar federation__Scope
directive @requiresScopes(scopes: [federation__Scope]!) on FIELD_DEFINITION
scalar ${fedType}
directive ${directiveName}(${argName}: [${fedType}]!) on FIELD_DEFINITION
type Query {
a: Int
}
enum E {
A @requiresScopes(scopes: [])
A ${directiveName}(${argName}: [])
}
`,
name: 'invalidDefinition',
};
const result = composeAsFed2Subgraphs([invalidDefinition]);
expect(errors(result)[0]).toEqual([
"DIRECTIVE_DEFINITION_INVALID",
"[invalidDefinition] Invalid definition for directive \"@requiresScopes\": argument \"scopes\" should have type \"[[federation__Scope!]!]!\" but found type \"[federation__Scope]!\"",
`[invalidDefinition] Invalid definition for directive \"${directiveName}\": argument \"${argName}\" should have type \"[[${fedType}!]!]!\" but found type \"[${fedType}]!\"`,
]);
});

it('on invalid application', () => {
it.each(testsToRun)('on invalid application', ({ directiveName, argName }) => {
const invalidApplication = {
typeDefs: gql`
type Query {
a: Int
}
enum E {
A @requiresScopes(scopes: [])
A ${directiveName}(${argName}: [])
}
`,
name: 'invalidApplication',
};
const result = composeAsFed2Subgraphs([invalidApplication]);
expect(errors(result)[0]).toEqual([
"INVALID_GRAPHQL",
"[invalidApplication] Directive \"@requiresScopes\" may not be used on ENUM_VALUE.",
`[invalidApplication] Directive \"${directiveName}\" may not be used on ENUM_VALUE.`,
]);
});
});
Expand Down
Loading

0 comments on commit 6d700a7

Please sign in to comment.