diff --git a/.changeset/slimy-trams-search.md b/.changeset/slimy-trams-search.md new file mode 100644 index 000000000..a8c2d29ce --- /dev/null +++ b/.changeset/slimy-trams-search.md @@ -0,0 +1,9 @@ +--- +'hive': minor +--- + +Enable automatic retrieval of schema changes by comparing with the latest composable version. This has already been the default for new projects created after April 2024. + +Federation and schema stitching projects can now publish service schemas to the registry even if those schemas would break composition. This has also been the default behavior for new projects created after April 2024. + +To ensure every version publishd to the schema registry is composable, we recommend to first check the schema against the registry **before** publishing. diff --git a/integration-tests/tests/api/artifacts-cdn.spec.ts b/integration-tests/tests/api/artifacts-cdn.spec.ts index 0150b2848..4b4db1fe7 100644 --- a/integration-tests/tests/api/artifacts-cdn.spec.ts +++ b/integration-tests/tests/api/artifacts-cdn.spec.ts @@ -1117,11 +1117,10 @@ function runArtifactsCDNTests( 'access versioned contract artifact with valid credentials', async ({ expect }) => { const { createOrg, ownerToken } = await initSeed().createOwner(); - const { createProject, setFeatureFlag } = await createOrg(); - const { createTargetAccessToken, createCdnAccess, target, setNativeFederation } = - await createProject(ProjectType.Federation); - await setFeatureFlag('compareToPreviousComposableVersion', true); - await setNativeFederation(true); + const { createProject } = await createOrg(); + const { createTargetAccessToken, createCdnAccess, target } = await createProject( + ProjectType.Federation, + ); const writeToken = await createTargetAccessToken({}); diff --git a/integration-tests/tests/api/schema/check.spec.ts b/integration-tests/tests/api/schema/check.spec.ts index 70a2a9df4..40b6bce15 100644 --- a/integration-tests/tests/api/schema/check.spec.ts +++ b/integration-tests/tests/api/schema/check.spec.ts @@ -1925,12 +1925,8 @@ describe.concurrent( () => { test.concurrent('native federation', async () => { const { createOrg } = await initSeed().createOwner(); - const { createProject, setFeatureFlag } = await createOrg(); - const { createTargetAccessToken, setNativeFederation } = await createProject( - ProjectType.Federation, - ); - await setFeatureFlag('compareToPreviousComposableVersion', true); - await setNativeFederation(true); + const { createProject } = await createOrg(); + const { createTargetAccessToken } = await createProject(ProjectType.Federation); const token = await createTargetAccessToken({}); @@ -1976,11 +1972,10 @@ describe.concurrent( test.concurrent('legacy fed composition', async () => { const { createOrg } = await initSeed().createOwner(); - const { createProject, setFeatureFlag } = await createOrg(); + const { createProject } = await createOrg(); const { createTargetAccessToken, setNativeFederation } = await createProject( ProjectType.Federation, ); - await setFeatureFlag('compareToPreviousComposableVersion', false); await setNativeFederation(false); const token = await createTargetAccessToken({}); @@ -2020,58 +2015,6 @@ describe.concurrent( }), }); }); - - test.concurrent( - 'legacy fed composition with compareToPreviousComposableVersion=true', - async () => { - const { createOrg } = await initSeed().createOwner(); - const { createProject, setFeatureFlag } = await createOrg(); - const { createTargetAccessToken, setNativeFederation } = await createProject( - ProjectType.Federation, - ); - await setFeatureFlag('compareToPreviousComposableVersion', true); - await setNativeFederation(false); - - const token = await createTargetAccessToken({}); - - // @key(fields:) is invalid - should trigger a composition error - const sdl = /* GraphQL */ ` - type Query { - ping: String - pong: String - foo: User - } - - type User @key(fields: "uuid") { - id: ID! - } - `; - - // Publish schema with write rights - await token - .publishSchema({ - sdl, - service: 'serviceA', - url: 'http://localhost:4000', - }) - .then(r => r.expectNoGraphQLErrors()); - - const result = await token - .checkSchema(sdl, 'serviceA') - .then(r => r.expectNoGraphQLErrors()); - - expect(result.schemaCheck).toMatchObject({ - valid: false, - __typename: 'SchemaCheckError', - changes: expect.objectContaining({ - total: 0, - }), - errors: expect.objectContaining({ - total: 1, - }), - }); - }, - ); }, ); diff --git a/integration-tests/tests/api/schema/contracts-check.spec.ts b/integration-tests/tests/api/schema/contracts-check.spec.ts index b4993380b..c4b5a20ec 100644 --- a/integration-tests/tests/api/schema/contracts-check.spec.ts +++ b/integration-tests/tests/api/schema/contracts-check.spec.ts @@ -32,12 +32,8 @@ const CreateContractMutation = graphql(` test.concurrent('schema check with successful contract checks', async ({ expect }) => { const { createOrg, ownerToken } = await initSeed().createOwner(); - const { createProject, setFeatureFlag } = await createOrg(); - const { createTargetAccessToken, target, setNativeFederation } = await createProject( - ProjectType.Federation, - ); - await setFeatureFlag('compareToPreviousComposableVersion', true); - await setNativeFederation(true); + const { createProject } = await createOrg(); + const { createTargetAccessToken, target } = await createProject(ProjectType.Federation); // Create a token with write rights const writeToken = await createTargetAccessToken({}); @@ -99,12 +95,8 @@ test.concurrent('schema check with successful contract checks', async ({ expect test.concurrent('schema check with failing contract composition', async ({ expect }) => { const { createOrg, ownerToken } = await initSeed().createOwner(); - const { createProject, setFeatureFlag } = await createOrg(); - const { createTargetAccessToken, target, setNativeFederation } = await createProject( - ProjectType.Federation, - ); - await setFeatureFlag('compareToPreviousComposableVersion', true); - await setNativeFederation(true); + const { createProject } = await createOrg(); + const { createTargetAccessToken, target } = await createProject(ProjectType.Federation); // Create a token with write rights const writeToken = await createTargetAccessToken({}); @@ -180,12 +172,8 @@ test.concurrent( 'schema check with failing contract composition (multiple contracts)', async ({ expect }) => { const { createOrg, ownerToken } = await initSeed().createOwner(); - const { createProject, setFeatureFlag } = await createOrg(); - const { createTargetAccessToken, target, setNativeFederation } = await createProject( - ProjectType.Federation, - ); - await setFeatureFlag('compareToPreviousComposableVersion', true); - await setNativeFederation(true); + const { createProject } = await createOrg(); + const { createTargetAccessToken, target } = await createProject(ProjectType.Federation); // Create a token with write rights const writeToken = await createTargetAccessToken({}); @@ -381,12 +369,10 @@ test.concurrent( 'approve failed schema check that has breaking change in contract check -> updates the status to successful and attaches meta information to the breaking change', async ({ expect }) => { const { createOrg, ownerToken } = await initSeed().createOwner(); - const { createProject, organization, setFeatureFlag } = await createOrg(); - const { createTargetAccessToken, project, target, setNativeFederation } = await createProject( + const { createProject, organization } = await createOrg(); + const { createTargetAccessToken, project, target } = await createProject( ProjectType.Federation, ); - await setFeatureFlag('compareToPreviousComposableVersion', true); - await setNativeFederation(true); // Create a token with write rights const writeToken = await createTargetAccessToken({}); @@ -547,8 +533,6 @@ test.concurrent( const { createTargetAccessToken, project, target, setNativeFederation } = await createProject( ProjectType.Federation, ); - await setFeatureFlag('compareToPreviousComposableVersion', true); - await setNativeFederation(true); // Create a token with write rights const writeToken = await createTargetAccessToken({}); @@ -739,12 +723,10 @@ test.concurrent( 'approving a schema check with contextId containing breaking changes does not allow the changes for subsequent checks with a different contextId', async ({ expect }) => { const { createOrg, ownerToken } = await initSeed().createOwner(); - const { createProject, organization, setFeatureFlag } = await createOrg(); - const { createTargetAccessToken, project, target, setNativeFederation } = await createProject( + const { createProject, organization } = await createOrg(); + const { createTargetAccessToken, project, target } = await createProject( ProjectType.Federation, ); - await setFeatureFlag('compareToPreviousComposableVersion', true); - await setNativeFederation(true); // Create a token with write rights const writeToken = await createTargetAccessToken({}); @@ -928,12 +910,10 @@ test.concurrent( 'subsequent schema check with shared contextId that contains new breaking changes that have not been approved fails', async ({ expect }) => { const { createOrg, ownerToken } = await initSeed().createOwner(); - const { createProject, organization, setFeatureFlag } = await createOrg(); - const { createTargetAccessToken, project, target, setNativeFederation } = await createProject( + const { createProject, organization } = await createOrg(); + const { createTargetAccessToken, project, target } = await createProject( ProjectType.Federation, ); - await setFeatureFlag('compareToPreviousComposableVersion', true); - await setNativeFederation(true); // Create a token with write rights const writeToken = await createTargetAccessToken({}); @@ -1128,12 +1108,10 @@ test.concurrent( 'schema check that has no composition errors in contract check -> can be approved', async ({ expect }) => { const { createOrg, ownerToken } = await initSeed().createOwner(); - const { createProject, organization, setFeatureFlag } = await createOrg(); - const { createTargetAccessToken, project, target, setNativeFederation } = await createProject( + const { createProject, organization } = await createOrg(); + const { createTargetAccessToken, project, target } = await createProject( ProjectType.Federation, ); - await setFeatureFlag('compareToPreviousComposableVersion', true); - await setNativeFederation(true); // Create a token with write rights const writeToken = await createTargetAccessToken({}); @@ -1237,12 +1215,10 @@ test.concurrent( 'schema check that has composition errors in contract check -> can not be approved', async ({ expect }) => { const { createOrg, ownerToken } = await initSeed().createOwner(); - const { createProject, organization, setFeatureFlag } = await createOrg(); + const { createProject, organization } = await createOrg(); const { createTargetAccessToken, project, target, setNativeFederation } = await createProject( ProjectType.Federation, ); - await setFeatureFlag('compareToPreviousComposableVersion', true); - await setNativeFederation(true); // Create a token with write rights const writeToken = await createTargetAccessToken({}); diff --git a/integration-tests/tests/api/schema/contracts-publish.spec.ts b/integration-tests/tests/api/schema/contracts-publish.spec.ts index 6bfc1a8f6..6b2420947 100644 --- a/integration-tests/tests/api/schema/contracts-publish.spec.ts +++ b/integration-tests/tests/api/schema/contracts-publish.spec.ts @@ -67,12 +67,8 @@ test.concurrent( 'schema publish with successful initial contract composition', async ({ expect }) => { const { createOrg, ownerToken } = await initSeed().createOwner(); - const { createProject, setFeatureFlag } = await createOrg(); - const { createTargetAccessToken, target, setNativeFederation } = await createProject( - ProjectType.Federation, - ); - await setFeatureFlag('compareToPreviousComposableVersion', true); - await setNativeFederation(true); + const { createProject } = await createOrg(); + const { createTargetAccessToken, target } = await createProject(ProjectType.Federation); // Create a token with write rights const writeToken = await createTargetAccessToken({}); @@ -158,12 +154,8 @@ test.concurrent( test.concurrent('schema publish with failing initial contract composition', async ({ expect }) => { const { createOrg, ownerToken } = await initSeed().createOwner(); - const { createProject, setFeatureFlag } = await createOrg(); - const { createTargetAccessToken, target, setNativeFederation } = await createProject( - ProjectType.Federation, - ); - await setFeatureFlag('compareToPreviousComposableVersion', true); - await setNativeFederation(true); + const { createProject } = await createOrg(); + const { createTargetAccessToken, target } = await createProject(ProjectType.Federation); // Create a token with write rights const writeToken = await createTargetAccessToken({}); @@ -239,12 +231,8 @@ test.concurrent('schema publish with failing initial contract composition', asyn test.concurrent('schema publish with succeeding contract composition', async ({ expect }) => { const { createOrg, ownerToken } = await initSeed().createOwner(); - const { createProject, setFeatureFlag } = await createOrg(); - const { createTargetAccessToken, target, setNativeFederation } = await createProject( - ProjectType.Federation, - ); - await setFeatureFlag('compareToPreviousComposableVersion', true); - await setNativeFederation(true); + const { createProject } = await createOrg(); + const { createTargetAccessToken, target } = await createProject(ProjectType.Federation); // Create a token with write rights const writeToken = await createTargetAccessToken({}); @@ -330,12 +318,8 @@ test.concurrent('schema publish with succeeding contract composition', async ({ test.concurrent('schema publish with failing contract composition', async ({ expect }) => { const { createOrg, ownerToken } = await initSeed().createOwner(); - const { createProject, setFeatureFlag } = await createOrg(); - const { createTargetAccessToken, target, setNativeFederation } = await createProject( - ProjectType.Federation, - ); - await setFeatureFlag('compareToPreviousComposableVersion', true); - await setNativeFederation(true); + const { createProject } = await createOrg(); + const { createTargetAccessToken, target } = await createProject(ProjectType.Federation); // Create a token with write rights const writeToken = await createTargetAccessToken({}); @@ -421,12 +405,8 @@ test.concurrent( 'schema delete with successful initial contract composition', async ({ expect }) => { const { createOrg, ownerToken } = await initSeed().createOwner(); - const { createProject, setFeatureFlag } = await createOrg(); - const { createTargetAccessToken, target, setNativeFederation } = await createProject( - ProjectType.Federation, - ); - await setFeatureFlag('compareToPreviousComposableVersion', true); - await setNativeFederation(true); + const { createProject } = await createOrg(); + const { createTargetAccessToken, target } = await createProject(ProjectType.Federation); // Create a token with write rights const writeToken = await createTargetAccessToken({}); @@ -512,12 +492,8 @@ test.concurrent( test.concurrent('schema delete with failing initial contract composition', async ({ expect }) => { const { createOrg, ownerToken } = await initSeed().createOwner(); - const { createProject, setFeatureFlag } = await createOrg(); - const { createTargetAccessToken, target, setNativeFederation } = await createProject( - ProjectType.Federation, - ); - await setFeatureFlag('compareToPreviousComposableVersion', true); - await setNativeFederation(true); + const { createProject } = await createOrg(); + const { createTargetAccessToken, target } = await createProject(ProjectType.Federation); // Create a token with write rights const writeToken = await createTargetAccessToken({}); @@ -594,12 +570,8 @@ test.concurrent('schema delete with failing initial contract composition', async test.concurrent('schema delete with succeeding contract composition', async ({ expect }) => { const { createOrg, ownerToken } = await initSeed().createOwner(); - const { createProject, setFeatureFlag } = await createOrg(); - const { createTargetAccessToken, target, setNativeFederation } = await createProject( - ProjectType.Federation, - ); - await setFeatureFlag('compareToPreviousComposableVersion', true); - await setNativeFederation(true); + const { createProject } = await createOrg(); + const { createTargetAccessToken, target } = await createProject(ProjectType.Federation); // Create a token with write rights const writeToken = await createTargetAccessToken({}); @@ -684,12 +656,8 @@ test.concurrent('schema delete with succeeding contract composition', async ({ e test.concurrent('schema delete with failing contract composition', async ({ expect }) => { const { createOrg, ownerToken } = await initSeed().createOwner(); - const { createProject, setFeatureFlag } = await createOrg(); - const { createTargetAccessToken, target, setNativeFederation } = await createProject( - ProjectType.Federation, - ); - await setFeatureFlag('compareToPreviousComposableVersion', true); - await setNativeFederation(true); + const { createProject } = await createOrg(); + const { createTargetAccessToken, target } = await createProject(ProjectType.Federation); // Create a token with write rights const writeToken = await createTargetAccessToken({}); @@ -768,11 +736,10 @@ test.concurrent( 'successful contracts schema can be fetched from the CDN with CDN access token', async ({ expect }) => { const { createOrg, ownerToken } = await initSeed().createOwner(); - const { createProject, setFeatureFlag } = await createOrg(); - const { createTargetAccessToken, createCdnAccess, target, setNativeFederation } = - await createProject(ProjectType.Federation); - await setFeatureFlag('compareToPreviousComposableVersion', true); - await setNativeFederation(true); + const { createProject } = await createOrg(); + const { createTargetAccessToken, createCdnAccess, target } = await createProject( + ProjectType.Federation, + ); // Create a token with write rights const writeToken = await createTargetAccessToken({}); @@ -835,8 +802,6 @@ test.concurrent( const { createProject, setFeatureFlag } = await createOrg(); const { createTargetAccessToken, createCdnAccess, target, setNativeFederation } = await createProject(ProjectType.Federation); - await setFeatureFlag('compareToPreviousComposableVersion', true); - await setNativeFederation(true); // Create a token with write rights const writeToken = await createTargetAccessToken({}); @@ -904,11 +869,10 @@ const DisabledContractMutation = graphql(` test.concurrent('disable contract results in CDN artifacts being removed', async ({ expect }) => { const { createOrg, ownerToken } = await initSeed().createOwner(); - const { createProject, setFeatureFlag } = await createOrg(); - const { createTargetAccessToken, createCdnAccess, target, setNativeFederation } = - await createProject(ProjectType.Federation); - await setFeatureFlag('compareToPreviousComposableVersion', true); - await setNativeFederation(true); + const { createProject } = await createOrg(); + const { createTargetAccessToken, createCdnAccess, target } = await createProject( + ProjectType.Federation, + ); // Create a token with write rights const writeToken = await createTargetAccessToken({}); @@ -993,11 +957,10 @@ test.concurrent( 'disable contract delete succeeds if no version/CDN artifacts have been published yet', async ({ expect }) => { const { createOrg, ownerToken } = await initSeed().createOwner(); - const { createProject, setFeatureFlag } = await createOrg(); - const { createTargetAccessToken, createCdnAccess, target, setNativeFederation } = - await createProject(ProjectType.Federation); - await setFeatureFlag('compareToPreviousComposableVersion', true); - await setNativeFederation(true); + const { createProject } = await createOrg(); + const { createTargetAccessToken, createCdnAccess, target } = await createProject( + ProjectType.Federation, + ); // Create a token with write rights const writeToken = await createTargetAccessToken({}); @@ -1051,15 +1014,8 @@ test.concurrent( 'disable contract delete succeeds if no version/CDN artifacts have been published yet', async ({ expect }) => { const { createOrg, ownerToken } = await initSeed().createOwner(); - const { createProject, setFeatureFlag } = await createOrg(); - const { createTargetAccessToken, target, setNativeFederation } = await createProject( - ProjectType.Federation, - ); - await setFeatureFlag('compareToPreviousComposableVersion', true); - await setNativeFederation(true); - - // Create a token with write rights - const writeToken = await createTargetAccessToken({}); + const { createProject } = await createOrg(); + const { target } = await createProject(ProjectType.Federation); const createContractResult = await execute({ document: CreateContractMutation, diff --git a/integration-tests/tests/api/schema/delete.spec.ts b/integration-tests/tests/api/schema/delete.spec.ts index fe6746fca..ef28c82e7 100644 --- a/integration-tests/tests/api/schema/delete.spec.ts +++ b/integration-tests/tests/api/schema/delete.spec.ts @@ -223,7 +223,7 @@ test.concurrent( ); test.concurrent( - 'composition error is persisted in the database when the super schema schema is not composable', + 'composition error is persisted in the database when the supergraph is not composable', async ({ expect }) => { let storage: Awaited> | undefined = undefined; @@ -263,6 +263,7 @@ test.concurrent( url: 'http://localhost:4000/graphql', }) .then(r => r.expectNoGraphQLErrors()); + expect(publishService1Result.schemaPublish.__typename).toBe('SchemaPublishSuccess'); const publishService2Result = await readToken diff --git a/integration-tests/tests/api/schema/publish.spec.ts b/integration-tests/tests/api/schema/publish.spec.ts index b77c59564..5e94324c1 100644 --- a/integration-tests/tests/api/schema/publish.spec.ts +++ b/integration-tests/tests/api/schema/publish.spec.ts @@ -4001,8 +4001,6 @@ test.concurrent( const { createTargetAccessToken, setNativeFederation } = await createProject( ProjectType.Federation, ); - await setNativeFederation(true); - await setFeatureFlag('compareToPreviousComposableVersion', true); const readWriteToken = await createTargetAccessToken({}); @@ -4146,8 +4144,6 @@ describe.concurrent( const { createTargetAccessToken, setNativeFederation } = await createProject( ProjectType.Federation, ); - await setFeatureFlag('compareToPreviousComposableVersion', true); - await setNativeFederation(true); const token = await createTargetAccessToken({}); @@ -4294,8 +4290,6 @@ describe.concurrent( const { createTargetAccessToken, setNativeFederation } = await createProject( ProjectType.Federation, ); - await setFeatureFlag('compareToPreviousComposableVersion', false); - await setNativeFederation(false); const token = await createTargetAccessToken({}); @@ -4427,149 +4421,6 @@ describe.concurrent( linkToWebsite: result.schemaPublish.linkToWebsite, }); }); - - test.concurrent( - 'legacy fed composition with compareToPreviousComposableVersion=true', - async () => { - const { createOrg } = await initSeed().createOwner(); - const { createProject, setFeatureFlag } = await createOrg(); - const { createTargetAccessToken, setNativeFederation } = await createProject( - ProjectType.Federation, - ); - await setFeatureFlag('compareToPreviousComposableVersion', true); - await setNativeFederation(false); - - const token = await createTargetAccessToken({}); - - const validSdl = /* GraphQL */ ` - type Query { - ping: String - pong: String - foo: User - } - - type User @key(fields: "id") { - id: ID! - } - `; - - // @key(fields:) is invalid - should trigger a composition error - const invalidSdl = /* GraphQL */ ` - type Query { - ping: String - pong: String - foo: User - } - - type User @key(fields: "uuid") { - id: ID! - } - `; - - // Publish schema with write rights - const validPublish = await token - .publishSchema({ - sdl: validSdl, - service: 'serviceA', - url: 'http://localhost:4000', - }) - .then(r => r.expectNoGraphQLErrors()); - - expect(validPublish.schemaPublish).toMatchObject({ - valid: true, - linkToWebsite: expect.any(String), - }); - - const invalidPublish = await token - .publishSchema({ - sdl: invalidSdl, - service: 'serviceA', - url: 'http://localhost:4000', - }) - .then(r => r.expectNoGraphQLErrors()); - - expect(invalidPublish.schemaPublish).toMatchObject({ - valid: false, - linkToWebsite: expect.any(String), - }); - - const invalidSdlCheck = await token - .checkSchema(invalidSdl, 'serviceA') - .then(r => r.expectNoGraphQLErrors()); - - expect(invalidSdlCheck.schemaCheck).toMatchObject({ - valid: false, - __typename: 'SchemaCheckError', - changes: expect.objectContaining({ - total: 0, - }), - errors: expect.objectContaining({ - total: 1, - }), - }); - - const validSdlCheck = await token - .checkSchema(validSdl, 'serviceA') - .then(r => r.expectNoGraphQLErrors()); - - expect(validSdlCheck.schemaCheck).toMatchObject({ - valid: true, - __typename: 'SchemaCheckSuccess', - changes: expect.objectContaining({ - total: 0, - }), - }); - - const result = await token - .publishSchema({ - sdl: validSdl, - service: 'serviceA', - url: 'http://localhost:4000', - }) - .then(r => r.expectNoGraphQLErrors()); - - expect(result.schemaPublish).toMatchObject({ - valid: true, - linkToWebsite: expect.any(String), - }); - - if ( - !('linkToWebsite' in result.schemaPublish) || - !('linkToWebsite' in invalidPublish.schemaPublish) || - !('linkToWebsite' in validPublish.schemaPublish) - ) { - throw new Error('linkToWebsite not found'); - } - - // If the linkToWebsite is the same as one of the previous versions, - // the schema publish was ignored due to unchanged input schemas. - // It shouldn't be the case. - // That's what we're checking here. - - expect(result.schemaPublish.linkToWebsite).not.toEqual( - invalidPublish.schemaPublish.linkToWebsite, - ); - - expect(result.schemaPublish.linkToWebsite).not.toEqual( - validPublish.schemaPublish.linkToWebsite, - ); - - const ignoredResult = await token - .publishSchema({ - sdl: validSdl, - service: 'serviceA', - url: 'http://localhost:4000', - }) - .then(r => r.expectNoGraphQLErrors()); - - // This time the schema publish should be ignored - // and link to the previous version - expect(ignoredResult.schemaPublish).toMatchObject({ - valid: true, - linkToWebsite: result.schemaPublish.linkToWebsite, - }); - }, - ); }, ); diff --git a/integration-tests/tests/cli/dev.spec.ts b/integration-tests/tests/cli/dev.spec.ts index 711c2ad32..10641e314 100644 --- a/integration-tests/tests/cli/dev.spec.ts +++ b/integration-tests/tests/cli/dev.spec.ts @@ -175,10 +175,6 @@ describe('dev --remote', () => { const { secret } = await createTargetAccessToken({}); const cli = createCLI({ readwrite: secret, readonly: secret }); - // Once we ship native federation v2 composition by default, we can remove these two lines - await setFeatureFlag('compareToPreviousComposableVersion', true); - await setNativeFederation(true); - await cli.publish({ sdl: /* GraphQL */ ` extend schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key"]) @@ -255,10 +251,6 @@ describe('dev --remote', () => { const { secret } = await createTargetAccessToken({}); const cli = createCLI({ readwrite: secret, readonly: secret }); - // Once we ship native federation v2 composition by default, we can remove these two lines - await setFeatureFlag('compareToPreviousComposableVersion', true); - await setNativeFederation(true); - await cli.publish({ sdl: /* GraphQL */ ` extend schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key"]) diff --git a/integration-tests/tests/models/federation-native-forceLegacyCompositionInTargets.spec.ts b/integration-tests/tests/models/federation-native-forceLegacyCompositionInTargets.spec.ts index 23361b3b8..05e084e2c 100644 --- a/integration-tests/tests/models/federation-native-forceLegacyCompositionInTargets.spec.ts +++ b/integration-tests/tests/models/federation-native-forceLegacyCompositionInTargets.spec.ts @@ -14,8 +14,6 @@ const options = ['targetWithNativeComposition', 'targetWithLegacyComposition'] a describe('publish', () => { describe.concurrent.each(options)('%s', caseName => { - const legacyComposition = isLegacyComposition(caseName); - test.concurrent('accepted: composable', async () => { const { cli: { publish }, @@ -55,26 +53,23 @@ describe('publish', () => { }); }); - test.concurrent( - `${legacyComposition ? 'rejected' : 'accepted'}: not composable (graphql errors)`, - async () => { - const { - cli: { publish }, - } = await prepare(caseName); + test.concurrent(`accepted: not composable (graphql errors)`, async () => { + const { + cli: { publish }, + } = await prepare(caseName); - // non-composable - await publish({ - sdl: /* GraphQL */ ` - type Query { - topProduct: Product - } - `, - serviceName: 'products', - serviceUrl: 'http://products:3000/graphql', - expect: legacyComposition ? 'rejected' : 'latest', - }); - }, - ); + // non-composable + await publish({ + sdl: /* GraphQL */ ` + type Query { + topProduct: Product + } + `, + serviceName: 'products', + serviceUrl: 'http://products:3000/graphql', + expect: 'latest', + }); + }); test.concurrent('accepted: composable, previous version was not', async () => { const { diff --git a/integration-tests/tests/models/federation-publish.spec.ts b/integration-tests/tests/models/federation-publish.spec.ts index 805b8d6de..d41d08cd1 100644 --- a/integration-tests/tests/models/federation-publish.spec.ts +++ b/integration-tests/tests/models/federation-publish.spec.ts @@ -44,26 +44,23 @@ describe('publish', () => { }); }); - test.concurrent( - `${legacyComposition ? 'rejected' : 'accepted'}: not composable (graphql errors)`, - async () => { - const { - cli: { publish }, - } = await prepare(ffs, legacyComposition); + test.concurrent(`accepted: not composable (graphql errors)`, async () => { + const { + cli: { publish }, + } = await prepare(ffs, legacyComposition); - // non-composable - await publish({ - sdl: /* GraphQL */ ` - type Query { - topProduct: Product - } - `, - serviceName: 'products', - serviceUrl: 'http://products:3000/graphql', - expect: legacyComposition ? 'rejected' : 'latest', - }); - }, - ); + // non-composable + await publish({ + sdl: /* GraphQL */ ` + type Query { + topProduct: Product + } + `, + serviceName: 'products', + serviceUrl: 'http://products:3000/graphql', + expect: 'latest', + }); + }); test.concurrent('accepted: composable, previous version was not', async () => { const { diff --git a/integration-tests/tests/models/federation-utils.ts b/integration-tests/tests/models/federation-utils.ts index 921f0c8e3..3be7c34a1 100644 --- a/integration-tests/tests/models/federation-utils.ts +++ b/integration-tests/tests/models/federation-utils.ts @@ -7,10 +7,6 @@ export type FeatureFlags = [string, FFValue][]; export const cases = [ ['default' as const, [] as FeatureFlags], - [ - 'compareToPreviousComposableVersion' as const, - [['compareToPreviousComposableVersion', true]] as FeatureFlags, - ], ['@apollo/federation' as const, [] as FeatureFlags], ] as const; diff --git a/integration-tests/tests/models/single.spec.ts b/integration-tests/tests/models/single.spec.ts index 78826b6d0..3b9bc5fb6 100644 --- a/integration-tests/tests/models/single.spec.ts +++ b/integration-tests/tests/models/single.spec.ts @@ -3,288 +3,118 @@ import { normalizeCliOutput } from '../../../scripts/serializers/cli-output'; import { createCLI } from '../../testkit/cli'; import { prepareProject } from '../../testkit/registry-models'; -const cases = [ - ['default' as const, [] as [string, boolean][]], - [ - 'compareToPreviousComposableVersion' as const, - [['compareToPreviousComposableVersion', true]] as [string, boolean][], - ], -] as Array<['default' | 'compareToPreviousComposableVersion', Array<[string, boolean]>]>; - describe('publish', () => { - describe.concurrent.each(cases)('%s', (caseName, ffs) => { - test.concurrent('accepted: composable', async () => { - const { - cli: { publish }, - } = await prepare(ffs); - await publish({ - sdl: `type Query { topProductName: String }`, - expect: 'latest-composable', - }); - }); - - test.concurrent('accepted: composable, breaking changes', async () => { - const { - cli: { publish }, - } = await prepare(ffs); - await publish({ - sdl: /* GraphQL */ ` - type Query { - topProductName: String - } - `, - expect: 'latest-composable', - }); - - await publish({ - sdl: /* GraphQL */ ` - type Query { - nooooo: String - } - `, - expect: 'latest-composable', - }); - }); - - test.concurrent( - `${caseName === 'default' ? 'rejected' : 'accepted'}: not composable (graphql errors)`, - async () => { - const { - cli: { publish }, - } = await prepare(ffs); - - await publish({ - sdl: /* GraphQL */ ` - type Query { - topProduct: Product - } - `, - expect: caseName === 'default' ? 'rejected' : 'latest', - }); - }, - ); - - test.concurrent('accepted: composable, no changes', async () => { - const { - cli: { publish }, - } = await prepare(ffs); - - // composable - await publish({ - sdl: /* GraphQL */ ` - type Query { - topProduct: String - } - `, - metadata: { version: 'v1' }, - expect: 'latest-composable', - }); - - // composable but no changes - await publish({ - sdl: /* GraphQL */ ` - type Query { - topProduct: String - } - `, - metadata: { version: 'v1' }, - expect: 'ignored', - }); - }); - - test.concurrent('accepted: composable, no changes but modified metadata', async () => { - const { - cli: { publish }, - } = await prepare(ffs); - - // composable - await publish({ - sdl: /* GraphQL */ ` - type Query { - topProduct: String - } - `, - metadata: { version: 'v1' }, - expect: 'latest-composable', - }); - - // composable but no changes with modified metadata - await publish({ - sdl: /* GraphQL */ ` - type Query { - topProduct: String - } - `, - metadata: { version: 'v2' }, - expect: 'latest-composable', - }); - }); - - test.concurrent('CLI output', async ({ expect }) => { - const { - cli: { publish }, - } = await prepare(ffs); - - let output = normalizeCliOutput( - (await publish({ - sdl: /* GraphQL */ ` - type Query { - topProduct: Product - } - - type Product { - id: ID! - name: String! - } - `, - expect: 'latest-composable', - })) ?? '', - ); - - expect(output).toEqual(expect.stringContaining(`v Published initial schema.`)); - expect(output).toEqual( - expect.stringContaining(`i Available at $appUrl/$organization/$project/$target`), - ); - - output = normalizeCliOutput( - (await publish({ - sdl: /* GraphQL */ ` - type Query { - topProduct: Product - } - - type Product { - id: ID! - name: String! - price: Int! - } - `, - expect: 'latest-composable', - })) ?? '', - ); - - expect(output).toEqual(expect.stringContaining(`v Schema published`)); - expect(output).toEqual( - expect.stringContaining( - `i Available at $appUrl/$organization/$project/$target/history/$version`, - ), - ); + test.concurrent('accepted: composable', async () => { + const { + cli: { publish }, + } = await prepare(); + await publish({ + sdl: `type Query { topProductName: String }`, + expect: 'latest-composable', }); }); -}); -describe('check', () => { - describe.concurrent.each(cases)('%s', (_, ffs) => { - test.concurrent('accepted: composable, no breaking changes', async () => { - const { - cli: { publish, check }, - } = await prepare(ffs); - - await publish({ - sdl: /* GraphQL */ ` - type Query { - topProduct: String - } - `, - expect: 'latest-composable', - }); - - const message = await check({ - sdl: /* GraphQL */ ` - type Query { - topProduct: String - topProductName: String - } - `, - expect: 'approved', - }); - - expect(message).toMatch('topProductName'); + test.concurrent('accepted: composable, breaking changes', async () => { + const { + cli: { publish }, + } = await prepare(); + await publish({ + sdl: /* GraphQL */ ` + type Query { + topProductName: String + } + `, + expect: 'latest-composable', }); - test.concurrent('accepted: no changes', async () => { - const { - cli: { publish, check }, - } = await prepare(ffs); + await publish({ + sdl: /* GraphQL */ ` + type Query { + nooooo: String + } + `, + expect: 'latest-composable', + }); + }); - await publish({ - sdl: /* GraphQL */ ` - type Query { - topProduct: String - } - `, - expect: 'latest-composable', - }); + test.concurrent(`accepted: not composable (graphql errors)`, async () => { + const { + cli: { publish }, + } = await prepare(); - await check({ - sdl: /* GraphQL */ ` - type Query { - topProduct: String - } - `, - expect: 'approved', - }); + await publish({ + sdl: /* GraphQL */ ` + type Query { + topProduct: Product + } + `, + expect: 'rejected', + }); + }); + + test.concurrent('accepted: composable, no changes', async () => { + const { + cli: { publish }, + } = await prepare(); + + // composable + await publish({ + sdl: /* GraphQL */ ` + type Query { + topProduct: String + } + `, + metadata: { version: 'v1' }, + expect: 'latest-composable', }); - test.concurrent('rejected: composable, breaking changes', async () => { - const { - cli: { publish, check }, - } = await prepare(ffs); + // composable but no changes + await publish({ + sdl: /* GraphQL */ ` + type Query { + topProduct: String + } + `, + metadata: { version: 'v1' }, + expect: 'ignored', + }); + }); - await publish({ - sdl: /* GraphQL */ ` - type Query { - topProduct: String - } - `, - expect: 'latest-composable', - }); + test.concurrent('accepted: composable, no changes but modified metadata', async () => { + const { + cli: { publish }, + } = await prepare(); - const message = await check({ - sdl: /* GraphQL */ ` - type Query { - topProductName: String - } - `, - expect: 'rejected', - }); - - expect(message).toMatch('removed'); + // composable + await publish({ + sdl: /* GraphQL */ ` + type Query { + topProduct: String + } + `, + metadata: { version: 'v1' }, + expect: 'latest-composable', }); - test.concurrent('rejected: not composable, no breaking changes', async () => { - const { - cli: { publish, check }, - } = await prepare(ffs); - - await publish({ - sdl: /* GraphQL */ ` - type Query { - topProduct: String - } - `, - expect: 'latest-composable', - }); - - const message = await check({ - sdl: /* GraphQL */ ` - type Query { - topProduct: String - topProductName: Strin - } - `, - expect: 'rejected', - }); - - expect(message).toMatch('Strin'); + // composable but no changes with modified metadata + await publish({ + sdl: /* GraphQL */ ` + type Query { + topProduct: String + } + `, + metadata: { version: 'v2' }, + expect: 'latest-composable', }); + }); - test.concurrent('rejected: not composable, breaking changes', async () => { - const { - cli: { publish, check }, - } = await prepare(ffs); + test.concurrent('CLI output', async ({ expect }) => { + const { + cli: { publish }, + } = await prepare(); - await publish({ + let output = normalizeCliOutput( + (await publish({ sdl: /* GraphQL */ ` type Query { topProduct: Product @@ -292,84 +122,235 @@ describe('check', () => { type Product { id: ID! - name: String + name: String! } `, expect: 'latest-composable', - }); + })) ?? '', + ); - const message = await check({ + expect(output).toEqual(expect.stringContaining(`v Published initial schema.`)); + expect(output).toEqual( + expect.stringContaining(`i Available at $appUrl/$organization/$project/$target`), + ); + + output = normalizeCliOutput( + (await publish({ sdl: /* GraphQL */ ` type Query { - product(id: ID!): Product + topProduct: Product } type Product { id: ID! - name: Str + name: String! + price: Int! } `, - expect: 'rejected', - }); + expect: 'latest-composable', + })) ?? '', + ); - expect(message).toMatch('Str'); + expect(output).toEqual(expect.stringContaining(`v Schema published`)); + expect(output).toEqual( + expect.stringContaining( + `i Available at $appUrl/$organization/$project/$target/history/$version`, + ), + ); + }); +}); + +describe('check', () => { + test.concurrent('accepted: composable, no breaking changes', async () => { + const { + cli: { publish, check }, + } = await prepare(); + + await publish({ + sdl: /* GraphQL */ ` + type Query { + topProduct: String + } + `, + expect: 'latest-composable', }); + + const message = await check({ + sdl: /* GraphQL */ ` + type Query { + topProduct: String + topProductName: String + } + `, + expect: 'approved', + }); + + expect(message).toMatch('topProductName'); + }); + + test.concurrent('accepted: no changes', async () => { + const { + cli: { publish, check }, + } = await prepare(); + + await publish({ + sdl: /* GraphQL */ ` + type Query { + topProduct: String + } + `, + expect: 'latest-composable', + }); + + await check({ + sdl: /* GraphQL */ ` + type Query { + topProduct: String + } + `, + expect: 'approved', + }); + }); + + test.concurrent('rejected: composable, breaking changes', async () => { + const { + cli: { publish, check }, + } = await prepare(); + + await publish({ + sdl: /* GraphQL */ ` + type Query { + topProduct: String + } + `, + expect: 'latest-composable', + }); + + const message = await check({ + sdl: /* GraphQL */ ` + type Query { + topProductName: String + } + `, + expect: 'rejected', + }); + + expect(message).toMatch('removed'); + }); + + test.concurrent('rejected: not composable, no breaking changes', async () => { + const { + cli: { publish, check }, + } = await prepare(); + + await publish({ + sdl: /* GraphQL */ ` + type Query { + topProduct: String + } + `, + expect: 'latest-composable', + }); + + const message = await check({ + sdl: /* GraphQL */ ` + type Query { + topProduct: String + topProductName: Strin + } + `, + expect: 'rejected', + }); + + expect(message).toMatch('Strin'); + }); + + test.concurrent('rejected: not composable, breaking changes', async () => { + const { + cli: { publish, check }, + } = await prepare(); + + await publish({ + sdl: /* GraphQL */ ` + type Query { + topProduct: Product + } + + type Product { + id: ID! + name: String + } + `, + expect: 'latest-composable', + }); + + const message = await check({ + sdl: /* GraphQL */ ` + type Query { + product(id: ID!): Product + } + + type Product { + id: ID! + name: Str + } + `, + expect: 'rejected', + }); + + expect(message).toMatch('Str'); }); }); describe('delete', () => { - describe.concurrent.each(cases)('%s', (_, ffs) => { - test.concurrent('not supported', async () => { - const { cli } = await prepare(ffs); + test.concurrent('not supported', async () => { + const { cli } = await prepare(); - await cli.delete({ - serviceName: 'test', - expect: 'rejected', - }); + await cli.delete({ + serviceName: 'test', + expect: 'rejected', }); }); }); describe('others', () => { - describe.concurrent.each(cases)('%s', (_, ffs) => { - test.concurrent('metadata should always be published as an array', async () => { - const { cli, cdn } = await prepare(ffs); + test.concurrent('metadata should always be published as an array', async () => { + const { cli, cdn } = await prepare(); - await cli.publish({ - sdl: /* GraphQL */ ` - type Query { - topProduct: String - } - `, - metadata: { version: 'v1' }, - expect: 'latest-composable', - }); - - await expect(cdn.fetchMetadata()).resolves.toEqual( - expect.objectContaining({ - status: 200, - body: { version: 'v1' }, // not an array - }), - ); - - await cli.publish({ - sdl: /* GraphQL */ ` - type Query { - topProduct: String - topProducts: [String] - } - `, - metadata: { version: 'v2' }, - expect: 'latest-composable', - }); - - await expect(cdn.fetchMetadata()).resolves.toEqual( - expect.objectContaining({ - status: 200, - body: { version: 'v2' }, // not an array - }), - ); + await cli.publish({ + sdl: /* GraphQL */ ` + type Query { + topProduct: String + } + `, + metadata: { version: 'v1' }, + expect: 'latest-composable', }); + + await expect(cdn.fetchMetadata()).resolves.toEqual( + expect.objectContaining({ + status: 200, + body: { version: 'v1' }, // not an array + }), + ); + + await cli.publish({ + sdl: /* GraphQL */ ` + type Query { + topProduct: String + topProducts: [String] + } + `, + metadata: { version: 'v2' }, + expect: 'latest-composable', + }); + + await expect(cdn.fetchMetadata()).resolves.toEqual( + expect.objectContaining({ + status: 200, + body: { version: 'v2' }, // not an array + }), + ); }); }); diff --git a/integration-tests/tests/models/stitching.spec.ts b/integration-tests/tests/models/stitching.spec.ts index bcf256287..2998e355a 100644 --- a/integration-tests/tests/models/stitching.spec.ts +++ b/integration-tests/tests/models/stitching.spec.ts @@ -3,798 +3,773 @@ import { normalizeCliOutput } from '../../../scripts/serializers/cli-output'; import { createCLI } from '../../testkit/cli'; import { prepareProject } from '../../testkit/registry-models'; -const cases = [ - ['default' as const, [] as [string, boolean][]], - [ - 'compareToPreviousComposableVersion' as const, - [['compareToPreviousComposableVersion', true]] as [string, boolean][], - ], -] as Array<['default' | 'compareToPreviousComposableVersion', Array<[string, boolean]>]>; - describe('publish', () => { - describe.concurrent.each(cases)('%s', (caseName, ffs) => { - test.concurrent('accepted: composable', async () => { - const { - cli: { publish }, - } = await prepare(ffs); - await publish({ - sdl: `type Query { topProductName: String }`, - serviceName: 'products', - serviceUrl: 'http://products:3000/graphql', - expect: 'latest-composable', - }); - }); - - test.concurrent('accepted: composable, breaking changes', async () => { - const { - cli: { publish }, - } = await prepare(ffs); - await publish({ - sdl: /* GraphQL */ ` - type Query { - topProductName: String - } - `, - serviceName: 'products', - serviceUrl: 'http://products:3000/graphql', - expect: 'latest-composable', - }); - - await publish({ - sdl: /* GraphQL */ ` - type Query { - nooooo: String - } - `, - serviceName: 'products', - serviceUrl: 'http://products:3000/graphql', - expect: 'latest-composable', - }); - }); - - test.concurrent( - `${caseName === 'default' ? 'rejected' : 'accepted'}: not composable (build errors)`, - async () => { - const { - cli: { publish }, - } = await prepare(ffs); - await publish({ - sdl: /* GraphQL */ ` - type Query { - topProductName: UnknownType - } - `, - serviceName: 'products', - serviceUrl: 'http://products:3000/graphql', - expect: caseName === 'default' ? 'rejected' : 'latest', - }); - }, - ); - - test.concurrent('accepted: composable, previous version was not', async () => { - const { - cli: { publish }, - } = await prepare(ffs); - - // non-composable - await publish({ - sdl: /* GraphQL */ ` - type Query { - product(id: ID!): Product - } - - type Product @key(selectionSet: "{ id") { - id: ID! - name: String - } - `, - serviceName: 'products', - serviceUrl: 'http://products:3000/graphql', - expect: 'latest', - }); - - // composable - await publish({ - sdl: /* GraphQL */ ` - type Query { - product(id: ID!): Product - } - - type Product @key(selectionSet: "{ id }") { - id: ID! - name: String - } - `, - serviceName: 'products', - serviceUrl: 'http://products:3000/graphql', - expect: 'latest-composable', - }); - }); - - test.concurrent('accepted: composable, no changes', async () => { - const { - cli: { publish }, - } = await prepare(ffs); - - // composable - await publish({ - sdl: /* GraphQL */ ` - type Query { - topProduct: String - } - `, - serviceName: 'products', - serviceUrl: 'http://products:3000/graphql', - metadata: { products: 3000 }, - expect: 'latest-composable', - }); - - // composable but no changes - await publish({ - sdl: /* GraphQL */ ` - type Query { - topProduct: String - } - `, - serviceName: 'products', - serviceUrl: 'http://products:3000/graphql', - metadata: { products: 3000 }, - expect: 'ignored', - }); - }); - - test.concurrent('accepted: composable, new url', async () => { - const { - cli: { publish }, - } = await prepare(ffs); - - // composable - await publish({ - sdl: /* GraphQL */ ` - type Query { - topProduct: String - } - `, - serviceName: 'products', - serviceUrl: 'http://products:3000/graphql', - expect: 'latest-composable', - }); - - // composable, no changes, only url is different - await publish({ - sdl: /* GraphQL */ ` - type Query { - topProduct: String - } - `, - serviceName: 'products', - serviceUrl: 'http://products:4321/graphql', // new url - expect: 'latest-composable', - }); - }); - - test.concurrent('accepted: composable, new metadata', async () => { - const { - cli: { publish }, - } = await prepare(ffs); - - // composable - await publish({ - sdl: /* GraphQL */ ` - type Query { - topProduct: String - } - `, - serviceName: 'products', - serviceUrl: 'http://products:3000/graphql', - metadata: { version: 'v1' }, - expect: 'latest-composable', - }); - - // composable, no changes, only metadata is different - await publish({ - sdl: /* GraphQL */ ` - type Query { - topProduct: String - } - `, - serviceName: 'products', - serviceUrl: 'http://products:3000/graphql', - metadata: { version: 'v2' }, // new metadata - expect: 'latest-composable', - }); - }); - - test.concurrent('rejected: missing service name', async () => { - const { - cli: { publish }, - } = await prepare(ffs); - - // composable - await publish({ - sdl: /* GraphQL */ ` - type Query { - topProduct: String - } - `, - serviceUrl: 'http://products:3000/graphql', - expect: 'rejected', - }); - }); - - test.concurrent('rejected: missing service url', async () => { - const { - cli: { publish }, - } = await prepare(ffs); - - // composable - await publish({ - sdl: /* GraphQL */ ` - type Query { - topProduct: String - } - `, - serviceName: 'products', - expect: 'rejected', - }); - }); - - test.concurrent('CLI output', async ({ expect }) => { - const { - cli: { publish }, - } = await prepare(ffs); - - const service = { - serviceName: 'products', - serviceUrl: 'http://products:3000/graphql', - }; - - let output = normalizeCliOutput( - (await publish({ - sdl: /* GraphQL */ ` - type Query { - topProduct: Product - } - - type Product { - id: ID! - name: String! - } - `, - ...service, - expect: 'latest-composable', - })) ?? '', - ); - - expect(output).toEqual(expect.stringContaining('v Published initial schema.')); - expect(output).toEqual( - expect.stringContaining('i Available at $appUrl/$organization/$project/$target'), - ); - - output = normalizeCliOutput( - (await publish({ - sdl: /* GraphQL */ ` - type Query { - topProduct: Product - } - - type Product { - id: ID! - name: String! - price: Int! - } - `, - ...service, - expect: 'latest-composable', - })) ?? '', - ); - - expect(output).toEqual(expect.stringContaining(`v Schema published`)); - expect(output).toEqual( - expect.stringContaining( - `i Available at $appUrl/$organization/$project/$target/history/$version`, - ), - ); + test.concurrent('accepted: composable', async () => { + const { + cli: { publish }, + } = await prepare(); + await publish({ + sdl: `type Query { topProductName: String }`, + serviceName: 'products', + serviceUrl: 'http://products:3000/graphql', + expect: 'latest-composable', }); }); - describe('check', () => { - describe.concurrent.each(cases)('%s', (caseName, ffs) => { - test.concurrent('accepted: composable, no breaking changes', async () => { - const { - cli: { publish, check }, - } = await prepare(ffs); + test.concurrent('accepted: composable, breaking changes', async () => { + const { + cli: { publish }, + } = await prepare(); + await publish({ + sdl: /* GraphQL */ ` + type Query { + topProductName: String + } + `, + serviceName: 'products', + serviceUrl: 'http://products:3000/graphql', + expect: 'latest-composable', + }); - await publish({ - sdl: /* GraphQL */ ` - type Query { - topProduct: String - } - `, - serviceName: 'products', - serviceUrl: 'http://products:3000/graphql', - expect: 'latest-composable', - }); + await publish({ + sdl: /* GraphQL */ ` + type Query { + nooooo: String + } + `, + serviceName: 'products', + serviceUrl: 'http://products:3000/graphql', + expect: 'latest-composable', + }); + }); - const message = await check({ - sdl: /* GraphQL */ ` - type Query { - topProduct: String - topProductName: String - } - `, - serviceName: 'products', - expect: 'approved', - }); + test.concurrent(`accepted: not composable (build errors)`, async () => { + const { + cli: { publish }, + } = await prepare(); + await publish({ + sdl: /* GraphQL */ ` + type Query { + topProductName: UnknownType + } + `, + serviceName: 'products', + serviceUrl: 'http://products:3000/graphql', + expect: 'latest', + }); + }); - expect(message).toMatch('topProductName'); - }); + test.concurrent('accepted: composable, previous version was not', async () => { + const { + cli: { publish }, + } = await prepare(); - test.concurrent('accepted: composable, previous version was not', async () => { - const { - cli: { publish, check }, - } = await prepare(ffs); + // non-composable + await publish({ + sdl: /* GraphQL */ ` + type Query { + product(id: ID!): Product + } - await publish({ - sdl: /* GraphQL */ ` - type Query { - product(id: ID!): Product - } + type Product @key(selectionSet: "{ id") { + id: ID! + name: String + } + `, + serviceName: 'products', + serviceUrl: 'http://products:3000/graphql', + expect: 'latest', + }); - type Product @key(selectionSet: "{ id") { - id: ID! - name: String - } - `, - serviceName: 'products', - serviceUrl: 'http://products:3000/graphql', - expect: 'latest', - }); + // composable + await publish({ + sdl: /* GraphQL */ ` + type Query { + product(id: ID!): Product + } - const message = await check({ - sdl: /* GraphQL */ ` - type Query { - product(id: ID!): Product - } + type Product @key(selectionSet: "{ id }") { + id: ID! + name: String + } + `, + serviceName: 'products', + serviceUrl: 'http://products:3000/graphql', + expect: 'latest-composable', + }); + }); - type Product @key(selectionSet: "{ id }") { - id: ID! - name: String - } - `, - serviceName: 'products', - expect: 'approved', - }); + test.concurrent('accepted: composable, no changes', async () => { + const { + cli: { publish }, + } = await prepare(); - expect(message).toMatch('No changes'); - }); + // composable + await publish({ + sdl: /* GraphQL */ ` + type Query { + topProduct: String + } + `, + serviceName: 'products', + serviceUrl: 'http://products:3000/graphql', + metadata: { products: 3000 }, + expect: 'latest-composable', + }); - test.concurrent('accepted: no changes', async () => { - const { - cli: { publish, check }, - } = await prepare(ffs); + // composable but no changes + await publish({ + sdl: /* GraphQL */ ` + type Query { + topProduct: String + } + `, + serviceName: 'products', + serviceUrl: 'http://products:3000/graphql', + metadata: { products: 3000 }, + expect: 'ignored', + }); + }); - await publish({ - sdl: /* GraphQL */ ` - type Query { - topProduct: String - } - `, - serviceName: 'products', - serviceUrl: 'http://products:3000/graphql', - expect: 'latest-composable', - }); + test.concurrent('accepted: composable, new url', async () => { + const { + cli: { publish }, + } = await prepare(); - await check({ - sdl: /* GraphQL */ ` - type Query { - topProduct: String - } - `, - serviceName: 'products', - expect: 'approved', - }); - }); + // composable + await publish({ + sdl: /* GraphQL */ ` + type Query { + topProduct: String + } + `, + serviceName: 'products', + serviceUrl: 'http://products:3000/graphql', + expect: 'latest-composable', + }); - test.concurrent('rejected: missing service name', async () => { - const { - cli: { check }, - } = await prepare(ffs); + // composable, no changes, only url is different + await publish({ + sdl: /* GraphQL */ ` + type Query { + topProduct: String + } + `, + serviceName: 'products', + serviceUrl: 'http://products:4321/graphql', // new url + expect: 'latest-composable', + }); + }); - const message = await check({ - sdl: /* GraphQL */ ` - type Query { - topProduct: String - } - `, - expect: 'rejected', - }); + test.concurrent('accepted: composable, new metadata', async () => { + const { + cli: { publish }, + } = await prepare(); - expect(message).toMatch('name'); - }); + // composable + await publish({ + sdl: /* GraphQL */ ` + type Query { + topProduct: String + } + `, + serviceName: 'products', + serviceUrl: 'http://products:3000/graphql', + metadata: { version: 'v1' }, + expect: 'latest-composable', + }); - test.concurrent('rejected: composable, breaking changes', async () => { - const { - cli: { publish, check }, - } = await prepare(ffs); + // composable, no changes, only metadata is different + await publish({ + sdl: /* GraphQL */ ` + type Query { + topProduct: String + } + `, + serviceName: 'products', + serviceUrl: 'http://products:3000/graphql', + metadata: { version: 'v2' }, // new metadata + expect: 'latest-composable', + }); + }); - await publish({ - sdl: /* GraphQL */ ` - type Query { - topProduct: String - } - `, - serviceName: 'products', - serviceUrl: 'http://products:3000/graphql', - expect: 'latest-composable', - }); + test.concurrent('rejected: missing service name', async () => { + const { + cli: { publish }, + } = await prepare(); - const message = await check({ - sdl: /* GraphQL */ ` - type Query { - topProductName: String - } - `, - serviceName: 'products', - expect: 'rejected', - }); + // composable + await publish({ + sdl: /* GraphQL */ ` + type Query { + topProduct: String + } + `, + serviceUrl: 'http://products:3000/graphql', + expect: 'rejected', + }); + }); - expect(message).toMatch('removed'); - }); + test.concurrent('rejected: missing service url', async () => { + const { + cli: { publish }, + } = await prepare(); - test.concurrent('rejected: not composable, no breaking changes', async () => { - const { - cli: { publish, check }, - } = await prepare(ffs); + // composable + await publish({ + sdl: /* GraphQL */ ` + type Query { + topProduct: String + } + `, + serviceName: 'products', + expect: 'rejected', + }); + }); - await publish({ - sdl: /* GraphQL */ ` - type Query { - topProduct: String - } - `, - serviceName: 'products', - serviceUrl: 'http://products:3000/graphql', - expect: 'latest-composable', - }); + test.concurrent('CLI output', async ({ expect }) => { + const { + cli: { publish }, + } = await prepare(); - const message = await check({ - sdl: /* GraphQL */ ` - type Query { - topProduct: String - topProductName: Strin - } - `, - serviceName: 'products', - expect: 'rejected', - }); + const service = { + serviceName: 'products', + serviceUrl: 'http://products:3000/graphql', + }; - expect(message).toMatch('Str'); - }); + let output = normalizeCliOutput( + (await publish({ + sdl: /* GraphQL */ ` + type Query { + topProduct: Product + } - test.concurrent('rejected: not composable, breaking changes (syntax error)', async () => { - const { - cli: { publish, check }, - } = await prepare(ffs); + type Product { + id: ID! + name: String! + } + `, + ...service, + expect: 'latest-composable', + })) ?? '', + ); - await publish({ - sdl: /* GraphQL */ ` - type Query { - topProduct: Product - } + expect(output).toEqual(expect.stringContaining('v Published initial schema.')); + expect(output).toEqual( + expect.stringContaining('i Available at $appUrl/$organization/$project/$target'), + ); - type Product @key(selectionSet: "{ id }") { - id: ID! - name: String - } - `, - serviceName: 'products', - serviceUrl: 'http://products:3000/graphql', - expect: 'latest-composable', - }); + output = normalizeCliOutput( + (await publish({ + sdl: /* GraphQL */ ` + type Query { + topProduct: Product + } - const message = await check({ - sdl: /* GraphQL */ ` - type Query { - product(id: ID!): Product - } + type Product { + id: ID! + name: String! + price: Int! + } + `, + ...service, + expect: 'latest-composable', + })) ?? '', + ); - type Product @key(selectionSet: "{ id") { - id: ID! - name: String - } - `, - serviceName: 'products', - expect: 'rejected', - }); + expect(output).toEqual(expect.stringContaining(`v Schema published`)); + expect(output).toEqual( + expect.stringContaining( + `i Available at $appUrl/$organization/$project/$target/history/$version`, + ), + ); + }); +}); - expect(message).toMatch('Expected Name'); - }); +describe('check', () => { + test.concurrent('accepted: composable, no breaking changes', async () => { + const { + cli: { publish, check }, + } = await prepare(); - test.concurrent('rejected: object type passed to input argument', async () => { - const { - cli: { publish, check }, - } = await prepare(ffs); + await publish({ + sdl: /* GraphQL */ ` + type Query { + topProduct: String + } + `, + serviceName: 'products', + serviceUrl: 'http://products:3000/graphql', + expect: 'latest-composable', + }); - await publish({ - sdl: /* GraphQL */ ` - type Query { - topProduct: Product - } + const message = await check({ + sdl: /* GraphQL */ ` + type Query { + topProduct: String + topProductName: String + } + `, + serviceName: 'products', + expect: 'approved', + }); - type Product @key(selectionSet: "{ id }") { - id: ID! - name: String - } - `, - serviceName: 'products', - serviceUrl: 'http://products:3000/graphql', - expect: 'latest-composable', - }); + expect(message).toMatch('topProductName'); + }); - await check({ - sdl: /* GraphQL */ ` - type Query { - topProduct(filter: TopProductFilter): Product - } + test.concurrent('accepted: composable, previous version was not', async () => { + const { + cli: { publish, check }, + } = await prepare(); - type Product @key(selectionSet: "{ id }") { - id: ID! - name: String - } + await publish({ + sdl: /* GraphQL */ ` + type Query { + product(id: ID!): Product + } - type TopProductFilter { - year: Int - category: String - } - `, - serviceName: 'products', - expect: 'rejected', - }); - }); + type Product @key(selectionSet: "{ id") { + id: ID! + name: String + } + `, + serviceName: 'products', + serviceUrl: 'http://products:3000/graphql', + expect: 'latest', + }); + + const message = await check({ + sdl: /* GraphQL */ ` + type Query { + product(id: ID!): Product + } + + type Product @key(selectionSet: "{ id }") { + id: ID! + name: String + } + `, + serviceName: 'products', + expect: 'approved', + }); + + expect(message).toMatch('No changes'); + }); + + test.concurrent('accepted: no changes', async () => { + const { + cli: { publish, check }, + } = await prepare(); + + await publish({ + sdl: /* GraphQL */ ` + type Query { + topProduct: String + } + `, + serviceName: 'products', + serviceUrl: 'http://products:3000/graphql', + expect: 'latest-composable', + }); + + await check({ + sdl: /* GraphQL */ ` + type Query { + topProduct: String + } + `, + serviceName: 'products', + expect: 'approved', + }); + }); + + test.concurrent('rejected: missing service name', async () => { + const { + cli: { check }, + } = await prepare(); + + const message = await check({ + sdl: /* GraphQL */ ` + type Query { + topProduct: String + } + `, + expect: 'rejected', + }); + + expect(message).toMatch('name'); + }); + + test.concurrent('rejected: composable, breaking changes', async () => { + const { + cli: { publish, check }, + } = await prepare(); + + await publish({ + sdl: /* GraphQL */ ` + type Query { + topProduct: String + } + `, + serviceName: 'products', + serviceUrl: 'http://products:3000/graphql', + expect: 'latest-composable', + }); + + const message = await check({ + sdl: /* GraphQL */ ` + type Query { + topProductName: String + } + `, + serviceName: 'products', + expect: 'rejected', + }); + + expect(message).toMatch('removed'); + }); + + test.concurrent('rejected: not composable, no breaking changes', async () => { + const { + cli: { publish, check }, + } = await prepare(); + + await publish({ + sdl: /* GraphQL */ ` + type Query { + topProduct: String + } + `, + serviceName: 'products', + serviceUrl: 'http://products:3000/graphql', + expect: 'latest-composable', + }); + + const message = await check({ + sdl: /* GraphQL */ ` + type Query { + topProduct: String + topProductName: Strin + } + `, + serviceName: 'products', + expect: 'rejected', + }); + + expect(message).toMatch('Str'); + }); + + test.concurrent('rejected: not composable, breaking changes (syntax error)', async () => { + const { + cli: { publish, check }, + } = await prepare(); + + await publish({ + sdl: /* GraphQL */ ` + type Query { + topProduct: Product + } + + type Product @key(selectionSet: "{ id }") { + id: ID! + name: String + } + `, + serviceName: 'products', + serviceUrl: 'http://products:3000/graphql', + expect: 'latest-composable', + }); + + const message = await check({ + sdl: /* GraphQL */ ` + type Query { + product(id: ID!): Product + } + + type Product @key(selectionSet: "{ id") { + id: ID! + name: String + } + `, + serviceName: 'products', + expect: 'rejected', + }); + + expect(message).toMatch('Expected Name'); + }); + + test.concurrent('rejected: object type passed to input argument', async () => { + const { + cli: { publish, check }, + } = await prepare(); + + await publish({ + sdl: /* GraphQL */ ` + type Query { + topProduct: Product + } + + type Product @key(selectionSet: "{ id }") { + id: ID! + name: String + } + `, + serviceName: 'products', + serviceUrl: 'http://products:3000/graphql', + expect: 'latest-composable', + }); + + await check({ + sdl: /* GraphQL */ ` + type Query { + topProduct(filter: TopProductFilter): Product + } + + type Product @key(selectionSet: "{ id }") { + id: ID! + name: String + } + + type TopProductFilter { + year: Int + category: String + } + `, + serviceName: 'products', + expect: 'rejected', }); }); }); describe('delete', () => { - describe.concurrent.each(cases)('%s', (caseName, ffs) => { - test.concurrent('accepted: composable before and after', async () => { - const { cli } = await prepare(ffs); + test.concurrent('accepted: composable before and after', async () => { + const { cli } = await prepare(); - await cli.publish({ - sdl: /* GraphQL */ ` - type Query { - topProduct: Product - } + await cli.publish({ + sdl: /* GraphQL */ ` + type Query { + topProduct: Product + } - type Product @key(selectionSet: "{ id }") { - id: ID! - name: String - } - `, - serviceName: 'products', - serviceUrl: 'http://products:3000/graphql', - expect: 'latest-composable', - }); - - await cli.publish({ - sdl: /* GraphQL */ ` - type Query { - topReview: Review - } - - type Review @key(selectionSet: "{ id }") { - id: ID! - title: String - } - `, - serviceName: 'reviews', - serviceUrl: 'http://reviews:3000/graphql', - expect: 'latest-composable', - }); - - const message = await cli.delete({ - serviceName: 'reviews', - expect: 'latest-composable', - }); - - expect(message).toMatch('reviews deleted'); + type Product @key(selectionSet: "{ id }") { + id: ID! + name: String + } + `, + serviceName: 'products', + serviceUrl: 'http://products:3000/graphql', + expect: 'latest-composable', }); - test.concurrent('rejected: unknown service', async () => { - const { cli } = await prepare(ffs); + await cli.publish({ + sdl: /* GraphQL */ ` + type Query { + topReview: Review + } - await cli.publish({ - sdl: /* GraphQL */ ` - type Query { - topProduct: Product - } - - type Product @key(selectionSet: "{ id }") { - id: ID! - name: String - } - `, - serviceName: 'products', - serviceUrl: 'http://products:3000/graphql', - expect: 'latest-composable', - }); - - const message = await cli.delete({ - serviceName: 'unknown_service', - expect: 'rejected', - }); - - expect(message).toMatch('not found'); + type Review @key(selectionSet: "{ id }") { + id: ID! + title: String + } + `, + serviceName: 'reviews', + serviceUrl: 'http://reviews:3000/graphql', + expect: 'latest-composable', }); + + const message = await cli.delete({ + serviceName: 'reviews', + expect: 'latest-composable', + }); + + expect(message).toMatch('reviews deleted'); + }); + + test.concurrent('rejected: unknown service', async () => { + const { cli } = await prepare(); + + await cli.publish({ + sdl: /* GraphQL */ ` + type Query { + topProduct: Product + } + + type Product @key(selectionSet: "{ id }") { + id: ID! + name: String + } + `, + serviceName: 'products', + serviceUrl: 'http://products:3000/graphql', + expect: 'latest-composable', + }); + + const message = await cli.delete({ + serviceName: 'unknown_service', + expect: 'rejected', + }); + + expect(message).toMatch('not found'); }); }); describe('other', () => { - describe.concurrent.each(cases)('%s', (_, ffs) => { - test.concurrent( - 'publish new schema when a field is moved from one service to another', - async () => { - const { tokens, fetchVersions, setFeatureFlag } = await prepareProject( - ProjectType.Stitching, - ); + test.concurrent( + 'publish new schema when a field is moved from one service to another', + async () => { + const { tokens, fetchVersions } = await prepareProject(ProjectType.Stitching); - for await (const [name, enabled] of ffs) { - await setFeatureFlag(name, enabled); - } + const { publish } = createCLI(tokens.registry); - const { publish } = createCLI(tokens.registry); + // cats service has only one field + await publish({ + sdl: /* GraphQL */ ` + type Query { + randomCat: String + } + `, + serviceName: 'cats', + serviceUrl: 'http://cats.com/graphql', + expect: 'latest-composable', + }); - // cats service has only one field - await publish({ - sdl: /* GraphQL */ ` - type Query { - randomCat: String - } - `, - serviceName: 'cats', - serviceUrl: 'http://cats.com/graphql', - expect: 'latest-composable', - }); + // dogs service has two fields + await publish({ + sdl: /* GraphQL */ ` + type Query { + randomDog: String + randomAnimal: String + } + `, + serviceName: 'dogs', + serviceUrl: 'http://dogs.com/graphql', + expect: 'latest-composable', + }); - // dogs service has two fields - await publish({ - sdl: /* GraphQL */ ` - type Query { - randomDog: String - randomAnimal: String - } - `, - serviceName: 'dogs', - serviceUrl: 'http://dogs.com/graphql', - expect: 'latest-composable', - }); + // cats service has now two fields, randomAnimal is borrowed from dogs service + await publish({ + sdl: /* GraphQL */ ` + type Query { + randomCat: String + randomAnimal: String + } + `, + serviceName: 'cats', + serviceUrl: 'http://cats.com/graphql', + expect: 'latest-composable', // We expect to have a new version, even tough the schema (merged) is the same + }); - // cats service has now two fields, randomAnimal is borrowed from dogs service - await publish({ - sdl: /* GraphQL */ ` - type Query { - randomCat: String - randomAnimal: String - } - `, - serviceName: 'cats', - serviceUrl: 'http://cats.com/graphql', - expect: 'latest-composable', // We expect to have a new version, even tough the schema (merged) is the same - }); + const versionsResult = await fetchVersions(3); + expect(versionsResult).toHaveLength(3); + }, + ); - const versionsResult = await fetchVersions(3); - expect(versionsResult).toHaveLength(3); - }, - ); + test.concurrent( + 'ignore stitching directive validation if the service overrides the stitching directive', + async () => { + const [spec, custom] = await Promise.all([prepare(), prepare()]); - test.concurrent( - 'ignore stitching directive validation if the service overrides the stitching directive', - async () => { - const [spec, custom] = await Promise.all([prepare(ffs), prepare(ffs)]); - - // Make sure validation works by publishing a schema - // with a stitching directive with incomplete selectionSet argument - await spec.cli.publish({ - sdl: /* GraphQL */ ` - type Query { - topProduct: Product - } - - type Product @key(selectionSet: "{ id ") { - id: ID! - name: String - } - `, - serviceName: 'products', - serviceUrl: 'http://products:3000/graphql', - expect: 'latest', // it's not composable because of the invalid selectionSet - }); - - // Stitching directive with incomplete selectionSet argument but a definition of @key - await custom.cli.publish({ - sdl: /* GraphQL */ ` - directive @key(selectionSet: String) on OBJECT - - type Query { - topProduct: Product - } - - type Product @key(selectionSet: "{ id ") { - id: ID! - name: String - } - `, - serviceName: 'products', - serviceUrl: 'http://products:3000/graphql', - expect: 'latest-composable', // it should be composable because the validation is skipped - }); - }, - ); - - test.concurrent('metadata should always be published as an array', async () => { - const { cli, cdn } = await prepare(ffs); - - await cli.publish({ + // Make sure validation works by publishing a schema + // with a stitching directive with incomplete selectionSet argument + await spec.cli.publish({ sdl: /* GraphQL */ ` type Query { topProduct: Product } - type Product @key(selectionSet: "{ id }") { + type Product @key(selectionSet: "{ id ") { id: ID! name: String } `, serviceName: 'products', serviceUrl: 'http://products:3000/graphql', - metadata: { products: 'v1' }, - expect: 'latest-composable', + expect: 'latest', // it's not composable because of the invalid selectionSet }); - await expect(cdn.fetchMetadata()).resolves.toEqual( - expect.objectContaining({ - status: 200, - body: [{ products: 'v1' }], // array - }), - ); - - await cli.publish({ + // Stitching directive with incomplete selectionSet argument but a definition of @key + await custom.cli.publish({ sdl: /* GraphQL */ ` + directive @key(selectionSet: String) on OBJECT + type Query { - topReview: Review + topProduct: Product } - type Review @key(selectionSet: "{ id }") { + type Product @key(selectionSet: "{ id ") { id: ID! - title: String + name: String } `, - serviceName: 'reviews', - serviceUrl: 'http://reviews:3000/graphql', - metadata: { reviews: 'v1' }, - expect: 'latest-composable', + serviceName: 'products', + serviceUrl: 'http://products:3000/graphql', + expect: 'latest-composable', // it should be composable because the validation is skipped }); + }, + ); - await expect(cdn.fetchMetadata()).resolves.toEqual( - expect.objectContaining({ - status: 200, - body: [{ products: 'v1' }, { reviews: 'v1' }], // array - }), - ); + test.concurrent('metadata should always be published as an array', async () => { + const { cli, cdn } = await prepare(); - await cli.delete({ - serviceName: 'reviews', - expect: 'latest-composable', - }); + await cli.publish({ + sdl: /* GraphQL */ ` + type Query { + topProduct: Product + } - await expect(cdn.fetchMetadata()).resolves.toEqual( - expect.objectContaining({ - status: 200, - body: [{ products: 'v1' }], // array - }), - ); + type Product @key(selectionSet: "{ id }") { + id: ID! + name: String + } + `, + serviceName: 'products', + serviceUrl: 'http://products:3000/graphql', + metadata: { products: 'v1' }, + expect: 'latest-composable', }); + + await expect(cdn.fetchMetadata()).resolves.toEqual( + expect.objectContaining({ + status: 200, + body: [{ products: 'v1' }], // array + }), + ); + + await cli.publish({ + sdl: /* GraphQL */ ` + type Query { + topReview: Review + } + + type Review @key(selectionSet: "{ id }") { + id: ID! + title: String + } + `, + serviceName: 'reviews', + serviceUrl: 'http://reviews:3000/graphql', + metadata: { reviews: 'v1' }, + expect: 'latest-composable', + }); + + await expect(cdn.fetchMetadata()).resolves.toEqual( + expect.objectContaining({ + status: 200, + body: [{ products: 'v1' }, { reviews: 'v1' }], // array + }), + ); + + await cli.delete({ + serviceName: 'reviews', + expect: 'latest-composable', + }); + + await expect(cdn.fetchMetadata()).resolves.toEqual( + expect.objectContaining({ + status: 200, + body: [{ products: 'v1' }], // array + }), + ); }); }); diff --git a/packages/services/api/src/modules/schema/providers/models/composite.ts b/packages/services/api/src/modules/schema/providers/models/composite.ts index db4d5826a..4a7d9bd9b 100644 --- a/packages/services/api/src/modules/schema/providers/models/composite.ts +++ b/packages/services/api/src/modules/schema/providers/models/composite.ts @@ -15,7 +15,6 @@ import { buildSchemaCheckFailureState, ContractCheckInput, ContractInput, - DeleteFailureReasonCode, isContractChecksSuccessful, PublishFailureReasonCode, PublishIgnoreReasonCode /* Check */, @@ -116,7 +115,6 @@ export class CompositeModel { contracts, failDiffOnDangerousChange, filterNestedChanges, - compareToLatestComposableVersion, }: { input: Pick & { // for a schema check the service url is optional @@ -144,7 +142,6 @@ export class CompositeModel { approvedChanges: Map; conditionalBreakingChangeDiffConfig: null | ConditionalBreakingChangeDiffConfig; failDiffOnDangerousChange: null | boolean; - compareToLatestComposableVersion: boolean; contracts: Array< ContractInput & { approvedChanges: Map | null; @@ -166,7 +163,6 @@ export class CompositeModel { const schemaSwapResult = latest ? swapServices(latest.schemas, incoming) : null; const schemas = schemaSwapResult ? schemaSwapResult.schemas : [incoming]; schemas.sort((a, b) => a.serviceName.localeCompare(b.serviceName)); - const comparedVersion = compareToLatestComposableVersion ? latestComposable : latest; const checksumCheck = await this.checks.checksum({ existing: schemaSwapResult?.existing @@ -207,7 +203,7 @@ export class CompositeModel { }); const previousVersionSdl = await this.checks.retrievePreviousVersionSdl({ - version: comparedVersion, + version: latestComposable, organization, project, targetId: selector.targetId, @@ -318,7 +314,6 @@ export class CompositeModel { contracts, conditionalBreakingChangeDiffConfig, failDiffOnDangerousChange, - compareToLatestComposableVersion, }: { input: { sdl: string; @@ -344,7 +339,6 @@ export class CompositeModel { contracts: Array | null; conditionalBreakingChangeDiffConfig: null | ConditionalBreakingChangeDiffConfig; failDiffOnDangerousChange: null | boolean; - compareToLatestComposableVersion: boolean; }): Promise { const latestSchemaVersion = latest; const latestServiceVersion = latest?.schemas?.find( @@ -382,9 +376,6 @@ export class CompositeModel { const schemas = schemaSwapResult?.schemas ?? [incoming]; schemas.sort((a, b) => a.serviceName.localeCompare(b.serviceName)); - const schemaVersionToCompareAgainst = compareToLatestComposableVersion - ? latestComposable - : latest; const checksumCheck = await this.checks.checksum({ existing: schemaSwapResult?.existing @@ -452,24 +443,8 @@ export class CompositeModel { }; } - if ( - compositionCheck.status === 'failed' && - compositionCheck.reason.errorsBySource.graphql.length > 0 && - !compareToLatestComposableVersion - ) { - return { - conclusion: SchemaPublishConclusion.Reject, - reasons: [ - { - code: PublishFailureReasonCode.CompositionFailure, - compositionErrors: compositionCheck.reason.errorsBySource.graphql, - }, - ], - }; - } - const previousVersionSdl = await this.checks.retrievePreviousVersionSdl({ - version: schemaVersionToCompareAgainst, + version: latestComposable, organization, project, targetId: target.id, @@ -491,7 +466,7 @@ export class CompositeModel { const diffCheck = await this.checks.diff({ conditionalBreakingChangeConfig: conditionalBreakingChangeDiffConfig, includeUrlChanges: { - schemasBefore: schemaVersionToCompareAgainst?.schemas ?? [], + schemasBefore: latestComposable?.schemas ?? [], schemasAfter: schemas, }, filterOutFederationChanges: project.type === ProjectType.FEDERATION, @@ -571,7 +546,6 @@ export class CompositeModel { conditionalBreakingChangeDiffConfig, contracts, failDiffOnDangerousChange, - compareToLatestComposableVersion, }: { input: { serviceName: string; @@ -597,7 +571,6 @@ export class CompositeModel { contracts: Array | null; conditionalBreakingChangeDiffConfig: null | ConditionalBreakingChangeDiffConfig; failDiffOnDangerousChange: null | boolean; - compareToLatestComposableVersion: boolean; }): Promise { const latestVersion = latest; @@ -623,7 +596,7 @@ export class CompositeModel { }); const previousVersionSdl = await this.checks.retrievePreviousVersionSdl({ - version: compareToLatestComposableVersion ? latestComposable : latest, + version: latestComposable, organization, project, targetId: selector.target, @@ -665,23 +638,6 @@ export class CompositeModel { getAffectedAppDeployments: getAffectedAppDeploymentsForDelete, }); - if ( - compositionCheck.status === 'failed' && - compositionCheck.reason.errorsBySource.graphql.length > 0 - ) { - if (!compareToLatestComposableVersion) { - return { - conclusion: SchemaDeleteConclusion.Reject, - reasons: [ - { - code: DeleteFailureReasonCode.CompositionFailure, - compositionErrors: compositionCheck.reason.errorsBySource.graphql, - }, - ], - }; - } - } - const { changes, breakingChanges } = diffCheck.status === 'failed' ? { diff --git a/packages/services/api/src/modules/schema/providers/models/single.ts b/packages/services/api/src/modules/schema/providers/models/single.ts index 7d2d805dd..5a7cad930 100644 --- a/packages/services/api/src/modules/schema/providers/models/single.ts +++ b/packages/services/api/src/modules/schema/providers/models/single.ts @@ -92,9 +92,6 @@ export class SingleModel { }; const schemas = [incoming] as [SingleSchemaInput]; - const comparedVersion = organization.featureFlags.compareToPreviousComposableVersion - ? latestComposable - : latest; const checksumResult = await this.checks.checksum({ existing: latest @@ -126,7 +123,7 @@ export class SingleModel { }); const previousVersionSdl = await this.checks.retrievePreviousVersionSdl({ - version: comparedVersion, + version: latestComposable, organization, project, targetId: selector.targetId, @@ -236,9 +233,6 @@ export class SingleModel { const latestVersion = latest; const schemas = [incoming] as [SingleSchemaInput]; - const compareToPreviousComposableVersion = - organization.featureFlags.compareToPreviousComposableVersion; - const comparedVersion = compareToPreviousComposableVersion ? latestComposable : latest; const checksumCheck = await this.checks.checksum({ existing: latest @@ -276,8 +270,23 @@ export class SingleModel { contracts: null, }); + if ( + compositionCheck.status === 'failed' && + compositionCheck.reason.errorsBySource.graphql.length > 0 + ) { + return { + conclusion: SchemaPublishConclusion.Reject, + reasons: [ + { + code: PublishFailureReasonCode.CompositionFailure, + compositionErrors: compositionCheck.reason.errorsBySource.graphql, + }, + ], + }; + } + const previousVersionSdl = await this.checks.retrievePreviousVersionSdl({ - version: comparedVersion, + version: latestComposable, organization, project, targetId: target.id, @@ -331,23 +340,6 @@ export class SingleModel { messages.push('Metadata has been updated'); } - if ( - compositionCheck.status === 'failed' && - compositionCheck.reason.errorsBySource.graphql.length > 0 - ) { - if (organization.featureFlags.compareToPreviousComposableVersion === false) { - return { - conclusion: SchemaPublishConclusion.Reject, - reasons: [ - { - code: PublishFailureReasonCode.CompositionFailure, - compositionErrors: compositionCheck.reason.errorsBySource.graphql, - }, - ], - }; - } - } - return { conclusion: SchemaPublishConclusion.Publish, state: { diff --git a/packages/services/api/src/modules/schema/providers/schema-manager.ts b/packages/services/api/src/modules/schema/providers/schema-manager.ts index ec342cdff..78959ddbb 100644 --- a/packages/services/api/src/modules/schema/providers/schema-manager.ts +++ b/packages/services/api/src/modules/schema/providers/schema-manager.ts @@ -1096,7 +1096,7 @@ export class SchemaManager { }; } - async getVersionBeforeVersionId(args: { + async getComposableVersionBeforeVersionId(args: { organization: string; project: string; target: string; @@ -1105,21 +1105,11 @@ export class SchemaManager { }) { this.logger.debug('Fetch version before version id. (args=%o)', args); - const [organization, project] = await Promise.all([ - this.storage.getOrganization({ - organizationId: args.organization, - }), - this.storage.getProject({ - organizationId: args.organization, - projectId: args.project, - }), - ]); - const schemaVersion = await this.storage.getVersionBeforeVersionId({ targetId: args.target, beforeVersionId: args.beforeVersionId, beforeVersionCreatedAt: args.beforeVersionCreatedAt, - onlyComposable: shouldUseLatestComposableVersion(args.target, project, organization), + onlyComposable: true, }); if (!schemaVersion) { @@ -1362,17 +1352,3 @@ export class SchemaManager { return null; } } - -export function shouldUseLatestComposableVersion( - targetId: string, - project: Project, - organization: Organization, -) { - return ( - organization.featureFlags.compareToPreviousComposableVersion || - // If the project is a native federation project, we should compare to the latest composable version - (project.nativeFederation && - // but only if the target is not forced to use the legacy composition - !organization.featureFlags.forceLegacyCompositionInTargets.includes(targetId)) - ); -} diff --git a/packages/services/api/src/modules/schema/providers/schema-publisher.ts b/packages/services/api/src/modules/schema/providers/schema-publisher.ts index faba509e5..c12f70d25 100644 --- a/packages/services/api/src/modules/schema/providers/schema-publisher.ts +++ b/packages/services/api/src/modules/schema/providers/schema-publisher.ts @@ -61,7 +61,7 @@ import { toCompositeSchemaInput, toSingleSchemaInput, } from './schema-helper'; -import { SchemaManager, shouldUseLatestComposableVersion } from './schema-manager'; +import { SchemaManager } from './schema-manager'; import { SchemaVersionHelper } from './schema-version-helper'; const schemaCheckCount = new promClient.Counter({ @@ -719,11 +719,6 @@ export class SchemaPublisher { conditionalBreakingChangeDiffConfig: conditionalBreakingChangeConfiguration?.conditionalBreakingChangeDiffConfig ?? null, failDiffOnDangerousChange, - compareToLatestComposableVersion: shouldUseLatestComposableVersion( - selector.targetId, - project, - organization, - ), filterNestedChanges: !input.schemaProposalId, }); break; @@ -1398,12 +1393,6 @@ export class SchemaPublisher { }), ]); - const compareToLatestComposableVersion = shouldUseLatestComposableVersion( - selector.targetId, - project, - organization, - ); - if (!latestVersion || latestVersion.schemas.length === 0) { throw new HiveError('Registry is empty'); } @@ -1476,18 +1465,8 @@ export class SchemaPublisher { conditionalBreakingChangeConfiguration?.conditionalBreakingChangeDiffConfig ?? null, contracts, failDiffOnDangerousChange, - compareToLatestComposableVersion, }); - let diffSchemaVersionId: string | null = null; - if (compareToLatestComposableVersion && latestComposableVersion) { - diffSchemaVersionId = latestComposableVersion.version.id; - } - - if (!compareToLatestComposableVersion && latestVersion) { - diffSchemaVersionId = latestVersion.version.id; - } - if (deleteResult.conclusion === SchemaDeleteConclusion.Accept) { this.logger.debug('Delete accepted'); if (input.dryRun !== true) { @@ -1497,7 +1476,7 @@ export class SchemaPublisher { targetId: selector.targetId, serviceName: input.serviceName, composable: deleteResult.state.composable, - diffSchemaVersionId, + diffSchemaVersionId: latestComposableVersion?.version.id ?? null, changes: deleteResult.state.changes, coordinatesDiff: deleteResult.state.coordinatesDiff, contracts: @@ -1771,15 +1750,6 @@ export class SchemaPublisher { }) : null; - const compareToLatestComposableVersion = shouldUseLatestComposableVersion( - target.id, - project, - organization, - ); - const comparedSchemaVersion = compareToLatestComposableVersion - ? latestComposable - : latestVersion; - const latestSchemaVersionContracts = latestVersion ? await this.contracts.getContractVersionsForSchemaVersion({ schemaVersionId: latestVersion.version.id, @@ -1867,7 +1837,6 @@ export class SchemaPublisher { conditionalBreakingChangeDiffConfig: conditionalBreakingChangeConfiguration?.conditionalBreakingChangeDiffConfig ?? null, failDiffOnDangerousChange, - compareToLatestComposableVersion: compareToLatestComposableVersion, }); break; default: { @@ -2009,8 +1978,6 @@ export class SchemaPublisher { const supergraph = publishResult.state.supergraph ?? null; - const diffSchemaVersionId = comparedSchemaVersion?.version.id ?? null; - this.logger.debug(`Assigning ${schemaLogIds.length} schemas to new version`); const serviceName = input.service; @@ -2065,7 +2032,7 @@ export class SchemaPublisher { }, changes, coordinatesDiff: publishResult.state.coordinatesDiff, - diffSchemaVersionId, + diffSchemaVersionId: latestComposable?.version.id ?? null, previousSchemaVersion: latestVersion?.version.id ?? null, conditionalBreakingChangeMetadata: await this.getConditionalBreakingChangeMetadata({ conditionalBreakingChangeConfiguration, diff --git a/packages/services/api/src/modules/schema/providers/schema-version-helper.ts b/packages/services/api/src/modules/schema/providers/schema-version-helper.ts index 46d9acf25..8113045ba 100644 --- a/packages/services/api/src/modules/schema/providers/schema-version-helper.ts +++ b/packages/services/api/src/modules/schema/providers/schema-version-helper.ts @@ -275,7 +275,7 @@ export class SchemaVersionHelper { return null; } - return await this.schemaManager.getVersionBeforeVersionId({ + return await this.schemaManager.getComposableVersionBeforeVersionId({ organization: schemaVersion.organizationId, project: schemaVersion.projectId, target: schemaVersion.targetId, diff --git a/packages/services/api/src/shared/entities.ts b/packages/services/api/src/shared/entities.ts index 220a43a50..18166b376 100644 --- a/packages/services/api/src/shared/entities.ts +++ b/packages/services/api/src/shared/entities.ts @@ -176,10 +176,6 @@ export interface Organization { }; getStarted: OrganizationGetStarted; featureFlags: { - /** - * @deprecated This feature flag is now a default for newly created organizations and projects. - */ - compareToPreviousComposableVersion: boolean; /** * Forces selected targets to use @apollo/federation library * when native composition is enabled for a project. diff --git a/packages/services/storage/src/index.ts b/packages/services/storage/src/index.ts index c8d2e2cbf..12d74919c 100644 --- a/packages/services/storage/src/index.ts +++ b/packages/services/storage/src/index.ts @@ -4902,7 +4902,6 @@ const decodeCDNAccessTokenRecord = (result: unknown): CDNAccessToken => { const FeatureFlagsModel = zod .object({ - compareToPreviousComposableVersion: zod.boolean().default(false), forceLegacyCompositionInTargets: zod.array(zod.string()).default([]), /** whether app deployments are enabled for the given organization */ appDeployments: zod.boolean().default(false), @@ -4916,7 +4915,6 @@ const FeatureFlagsModel = zod .transform( val => val ?? { - compareToPreviousComposableVersion: false, forceLegacyCompositionInTargets: [], appDeployments: false, otelTracing: false,