From 9aa7ff726dfb87a9fbcf1117471235fbf8865bc1 Mon Sep 17 00:00:00 2001 From: Kamil Kisiela Date: Mon, 11 Sep 2023 09:28:38 +0200 Subject: [PATCH] Native Federation v2 support --- integration-tests/package.json | 3 +- integration-tests/testkit/registry-models.ts | 9 +- integration-tests/testkit/seed.ts | 7 + .../tests/models/federation.spec.ts | 109 +++--- integration-tests/tests/models/single.spec.ts | 45 ++- .../tests/models/stitching.spec.ts | 50 ++- package.json | 2 +- packages/libraries/client/package.json | 2 +- .../2023.09.07T14.14.14.native-fed-v2.ts | 7 + packages/migrations/src/run-pg-migrations.ts | 2 + .../services/api/src/modules/lab/resolvers.ts | 25 +- .../providers/models/composite-legacy.ts | 4 + .../schema/providers/models/single-legacy.ts | 4 + .../modules/schema/providers/models/single.ts | 5 + .../providers/orchestrators/federation.ts | 11 +- .../schema/providers/orchestrators/single.ts | 1 - .../providers/orchestrators/stitching.ts | 1 - .../schema/providers/registry-checks.ts | 26 +- .../schema/providers/schema-manager.ts | 63 +++- .../schema/providers/schema-publisher.ts | 24 +- .../api/src/modules/schema/resolvers.ts | 124 +++++-- packages/services/api/src/shared/entities.ts | 6 +- packages/services/schema/package.json | 1 + packages/services/schema/src/api.ts | 67 ++-- packages/services/schema/src/orchestrators.ts | 123 +++++-- packages/services/storage/src/db/types.ts | 1 + packages/services/storage/src/index.ts | 1 + pnpm-lock.yaml | 345 +++++++++--------- scripts/serializer.ts | 21 +- scripts/serializers/cli-output.ts | 19 + 30 files changed, 718 insertions(+), 390 deletions(-) create mode 100644 packages/migrations/src/actions/2023.09.07T14.14.14.native-fed-v2.ts create mode 100644 scripts/serializers/cli-output.ts diff --git a/integration-tests/package.json b/integration-tests/package.json index cc4bcf01893..8a2f76df365 100644 --- a/integration-tests/package.json +++ b/integration-tests/package.json @@ -22,6 +22,7 @@ "@trpc/server": "10.31.0", "@types/dockerode": "3.3.19", "@types/ioredis": "4.28.10", + "@vitest/ui": "0.34.3", "@whatwg-node/fetch": "0.9.9", "bcryptjs": "2.4.3", "date-fns": "2.30.0", @@ -33,7 +34,7 @@ "slonik": "30.4.4", "strip-ansi": "7.1.0", "tslib": "2.5.3", - "vitest": "0.31.1", + "vitest": "0.34.3", "zod": "3.21.4" } } diff --git a/integration-tests/testkit/registry-models.ts b/integration-tests/testkit/registry-models.ts index d948b4f5535..a2dc31d6f90 100644 --- a/integration-tests/testkit/registry-models.ts +++ b/integration-tests/testkit/registry-models.ts @@ -8,12 +8,10 @@ export async function prepareProject( const { createOrg } = await initSeed().createOwner(); const { organization, createProject, setFeatureFlag, setOrganizationSchemaPolicy } = await createOrg(); - const { project, createToken, target, targets, setProjectSchemaPolicy } = await createProject( - projectType, - { + const { project, createToken, target, targets, setProjectSchemaPolicy, setNativeFederation } = + await createProject(projectType, { useLegacyRegistryModels: model === RegistryModel.Legacy, - }, - ); + }); // Create a token with write rights const { secret: readwriteToken } = await createToken({ @@ -46,5 +44,6 @@ export async function prepareProject( }, }, setFeatureFlag, + setNativeFederation, }; } diff --git a/integration-tests/testkit/seed.ts b/integration-tests/testkit/seed.ts index 154841ddcd0..cd8bbf66985 100644 --- a/integration-tests/testkit/seed.ts +++ b/integration-tests/testkit/seed.ts @@ -179,6 +179,13 @@ export function initSeed() { project, targets, target, + async setNativeFederation(enabled: boolean) { + const pool = await poolPromise; + + await pool.query(sql` + UPDATE public.projects SET native_federation = ${enabled} WHERE id = ${project.id} + `); + }, async setProjectSchemaPolicy(policy: SchemaPolicyInput) { const result = await execute({ document: UpdateSchemaPolicyForProject, diff --git a/integration-tests/tests/models/federation.spec.ts b/integration-tests/tests/models/federation.spec.ts index ae97473c236..29374894815 100644 --- a/integration-tests/tests/models/federation.spec.ts +++ b/integration-tests/tests/models/federation.spec.ts @@ -1,20 +1,33 @@ import { ProjectType, TargetAccessScope } from '@app/gql/graphql'; +import { normalizeCliOutput } from '../../../scripts/serializers/cli-output'; import { createCLI, schemaPublish } from '../../testkit/cli'; import { prepareProject } from '../../testkit/registry-models'; import { initSeed } from '../../testkit/seed'; const cases = [ - ['default' as const, [] as [string, boolean][]], + ['default' as const, [] as [string, boolean][], false], [ 'compareToPreviousComposableVersion' as const, [['compareToPreviousComposableVersion', true]] as [string, boolean][], + false, ], -] as Array<['default' | 'compareToPreviousComposableVersion', Array<[string, boolean]>]>; + [ + 'nativeFederation' as const, + [['compareToPreviousComposableVersion', true]] as [string, boolean][], + true, + ], +] as Array< + [ + 'default' | 'compareToPreviousComposableVersion' | 'nativeFederation', + Array<[string, boolean]>, + boolean, + ] +>; describe('publish', () => { - describe.each(cases)('%s', (caseName, ffs) => { + describe.concurrent.each(cases)('%s', (caseName, ffs, nativeFederation) => { test.concurrent('accepted: composable', async () => { - const { publish } = await prepare(ffs); + const { publish } = await prepare(ffs, nativeFederation); await publish({ sdl: `type Query { topProductName: String }`, serviceName: 'products', @@ -24,7 +37,7 @@ describe('publish', () => { }); test.concurrent('accepted: composable, breaking changes', async () => { - const { publish } = await prepare(ffs); + const { publish } = await prepare(ffs, nativeFederation); await publish({ sdl: /* GraphQL */ ` type Query { @@ -51,7 +64,7 @@ describe('publish', () => { test.concurrent( `${caseName === 'default' ? 'rejected' : 'accepted'}: not composable (graphql errors)`, async () => { - const { publish } = await prepare(ffs); + const { publish } = await prepare(ffs, nativeFederation); // non-composable await publish({ @@ -68,7 +81,7 @@ describe('publish', () => { ); test.concurrent('accepted: composable, previous version was not', async () => { - const { publish } = await prepare(ffs); + const { publish } = await prepare(ffs, nativeFederation); // non-composable await publish({ @@ -104,7 +117,7 @@ describe('publish', () => { }); test.concurrent('accepted: composable, no changes', async () => { - const { publish } = await prepare(ffs); + const { publish } = await prepare(ffs, nativeFederation); // composable await publish({ @@ -132,7 +145,7 @@ describe('publish', () => { }); test.concurrent('accepted: composable, new url', async () => { - const { publish } = await prepare(ffs); + const { publish } = await prepare(ffs, nativeFederation); // composable await publish({ @@ -160,7 +173,7 @@ describe('publish', () => { }); test.concurrent('rejected: missing service name', async () => { - const { publish } = await prepare(ffs); + const { publish } = await prepare(ffs, nativeFederation); // composable await publish({ @@ -175,7 +188,7 @@ describe('publish', () => { }); test.concurrent('rejected: missing service url', async () => { - const { publish } = await prepare(ffs); + const { publish } = await prepare(ffs, nativeFederation); // composable await publish({ @@ -190,15 +203,15 @@ describe('publish', () => { }); test.concurrent('CLI output', async ({ expect }) => { - const { publish } = await prepare(ffs); + const { publish } = await prepare(ffs, nativeFederation); const service = { serviceName: 'products', serviceUrl: 'http://products:3000/graphql', }; - await expect( - publish({ + let output = normalizeCliOutput( + (await publish({ sdl: /* GraphQL */ ` type Query { topProduct: Product @@ -211,14 +224,18 @@ describe('publish', () => { `, ...service, expect: 'latest-composable', - }), - ).resolves.toMatchInlineSnapshot(` - v Published initial schema. - i Available at http://localhost:8080/$organization/$project/production - `); - - await expect( - publish({ + })) ?? '', + ); + + expect(output).toEqual(expect.stringContaining(`v Published initial schema.`)); + expect(output).toEqual( + expect.stringContaining( + `i Available at http://localhost:8080/$organization/$project/production`, + ), + ); + + output = normalizeCliOutput( + (await publish({ sdl: /* GraphQL */ ` type Query { topProduct: Product @@ -232,19 +249,23 @@ describe('publish', () => { `, ...service, expect: 'latest-composable', - }), - ).resolves.toMatchInlineSnapshot(` - v Schema published - i Available at http://localhost:8080/$organization/$project/production/history/$version - `); + })) ?? '', + ); + + expect(output).toEqual(expect.stringContaining(`v Schema published`)); + expect(output).toEqual( + expect.stringContaining( + `i Available at http://localhost:8080/$organization/$project/production/history/$version`, + ), + ); }); }); }); describe('check', () => { - describe.each(cases)('%s', (_, ffs) => { + describe.concurrent.each(cases)('%s', (_, ffs, nativeFederation) => { test.concurrent('accepted: composable, no breaking changes', async () => { - const { publish, check } = await prepare(ffs); + const { publish, check } = await prepare(ffs, nativeFederation); await publish({ sdl: /* GraphQL */ ` @@ -272,7 +293,7 @@ describe('check', () => { }); test.concurrent('accepted: composable, previous version was not', async () => { - const { publish, check } = await prepare(ffs); + const { publish, check } = await prepare(ffs, nativeFederation); await publish({ sdl: /* GraphQL */ ` @@ -306,7 +327,7 @@ describe('check', () => { }); test.concurrent('accepted: no changes', async () => { - const { publish, check } = await prepare(ffs); + const { publish, check } = await prepare(ffs, nativeFederation); await publish({ sdl: /* GraphQL */ ` @@ -331,7 +352,7 @@ describe('check', () => { }); test.concurrent('rejected: missing service name', async () => { - const { check } = await prepare(ffs); + const { check } = await prepare(ffs, nativeFederation); const message = await check({ sdl: /* GraphQL */ ` @@ -346,7 +367,7 @@ describe('check', () => { }); test.concurrent('rejected: composable, breaking changes', async () => { - const { publish, check } = await prepare(ffs); + const { publish, check } = await prepare(ffs, nativeFederation); await publish({ sdl: /* GraphQL */ ` @@ -373,7 +394,7 @@ describe('check', () => { }); test.concurrent('rejected: not composable, no breaking changes', async () => { - const { publish, check } = await prepare(ffs); + const { publish, check } = await prepare(ffs, nativeFederation); await publish({ sdl: /* GraphQL */ ` @@ -401,7 +422,7 @@ describe('check', () => { }); test.concurrent('rejected: not composable, breaking changes', async () => { - const { publish, check } = await prepare(ffs); + const { publish, check } = await prepare(ffs, nativeFederation); await publish({ sdl: /* GraphQL */ ` @@ -441,9 +462,9 @@ describe('check', () => { }); describe('delete', () => { - describe.each(cases)('%s', (_, ffs) => { + describe.concurrent.each(cases)('%s', (_, ffs, nativeFederation) => { test.concurrent('accepted: composable before and after', async () => { - const cli = await prepare(ffs); + const cli = await prepare(ffs, nativeFederation); await cli.publish({ sdl: /* GraphQL */ ` @@ -486,7 +507,7 @@ describe('delete', () => { }); test.concurrent('rejected: unknown service', async () => { - const cli = await prepare(ffs); + const cli = await prepare(ffs, nativeFederation); await cli.publish({ sdl: /* GraphQL */ ` @@ -515,7 +536,7 @@ describe('delete', () => { }); describe('other', () => { - describe.each(cases)('%s', (_, ffs) => { + describe.concurrent.each(cases)('%s', (_, ffs) => { test.concurrent('service url should be available in supergraph', async () => { const { createOrg } = await initSeed().createOwner(); const { inviteAndJoinMember, createProject } = await createOrg(); @@ -718,12 +739,18 @@ describe('other', () => { }); }); -async function prepare(featureFlags: Array<[string, boolean]> = []) { - const { tokens, setFeatureFlag } = await prepareProject(ProjectType.Federation); +async function prepare(featureFlags: Array<[string, boolean]> = [], nativeFederation = false) { + const { tokens, setFeatureFlag, setNativeFederation } = await prepareProject( + ProjectType.Federation, + ); for await (const [name, enabled] of featureFlags) { await setFeatureFlag(name, enabled); } + if (nativeFederation === true) { + await setNativeFederation(true); + } + return createCLI(tokens.registry); } diff --git a/integration-tests/tests/models/single.spec.ts b/integration-tests/tests/models/single.spec.ts index 078cb4e014b..a043f0dee7e 100644 --- a/integration-tests/tests/models/single.spec.ts +++ b/integration-tests/tests/models/single.spec.ts @@ -1,4 +1,5 @@ import { ProjectType } from '@app/gql/graphql'; +import { normalizeCliOutput } from '../../../scripts/serializers/cli-output'; import { createCLI } from '../../testkit/cli'; import { prepareProject } from '../../testkit/registry-models'; @@ -11,7 +12,7 @@ const cases = [ ] as Array<['default' | 'compareToPreviousComposableVersion', Array<[string, boolean]>]>; describe('publish', () => { - describe.each(cases)('%s', (caseName, ffs) => { + describe.concurrent.each(cases)('%s', (caseName, ffs) => { test.concurrent('accepted: composable', async () => { const { publish } = await prepare(ffs); await publish({ @@ -84,8 +85,8 @@ describe('publish', () => { test.concurrent('CLI output', async ({ expect }) => { const { publish } = await prepare(ffs); - await expect( - publish({ + let output = normalizeCliOutput( + (await publish({ sdl: /* GraphQL */ ` type Query { topProduct: Product @@ -97,14 +98,18 @@ describe('publish', () => { } `, expect: 'latest-composable', - }), - ).resolves.toMatchInlineSnapshot(` - v Published initial schema. - i Available at http://localhost:8080/$organization/$project/production - `); - - await expect( - publish({ + })) ?? '', + ); + + expect(output).toEqual(expect.stringContaining(`v Published initial schema.`)); + expect(output).toEqual( + expect.stringContaining( + `i Available at http://localhost:8080/$organization/$project/production`, + ), + ); + + output = normalizeCliOutput( + (await publish({ sdl: /* GraphQL */ ` type Query { topProduct: Product @@ -117,17 +122,21 @@ describe('publish', () => { } `, expect: 'latest-composable', - }), - ).resolves.toMatchInlineSnapshot(` - v Schema published - i Available at http://localhost:8080/$organization/$project/production/history/$version - `); + })) ?? '', + ); + + expect(output).toEqual(expect.stringContaining(`v Schema published`)); + expect(output).toEqual( + expect.stringContaining( + `i Available at http://localhost:8080/$organization/$project/production/history/$version`, + ), + ); }); }); }); describe('check', () => { - describe.each(cases)('%s', (_, ffs) => { + describe.concurrent.each(cases)('%s', (_, ffs) => { test.concurrent('accepted: composable, no breaking changes', async () => { const { publish, check } = await prepare(ffs); @@ -261,7 +270,7 @@ describe('check', () => { }); describe('delete', () => { - describe.each(cases)('%s', (_, ffs) => { + describe.concurrent.each(cases)('%s', (_, ffs) => { test.concurrent('not supported', async () => { const cli = await prepare(ffs); diff --git a/integration-tests/tests/models/stitching.spec.ts b/integration-tests/tests/models/stitching.spec.ts index d5a02b3137d..2d7747194a5 100644 --- a/integration-tests/tests/models/stitching.spec.ts +++ b/integration-tests/tests/models/stitching.spec.ts @@ -1,4 +1,5 @@ import { ProjectType } from '@app/gql/graphql'; +import { normalizeCliOutput } from '../../../scripts/serializers/cli-output'; import { createCLI } from '../../testkit/cli'; import { prepareProject } from '../../testkit/registry-models'; @@ -11,7 +12,7 @@ const cases = [ ] as Array<['default' | 'compareToPreviousComposableVersion', Array<[string, boolean]>]>; describe('publish', () => { - describe.each(cases)('%s', (caseName, ffs) => { + describe.concurrent.each(cases)('%s', (caseName, ffs) => { test.concurrent('accepted: composable', async () => { const { publish } = await prepare(ffs); await publish({ @@ -196,8 +197,8 @@ describe('publish', () => { serviceUrl: 'http://products:3000/graphql', }; - await expect( - publish({ + let output = normalizeCliOutput( + (await publish({ sdl: /* GraphQL */ ` type Query { topProduct: Product @@ -210,14 +211,18 @@ describe('publish', () => { `, ...service, expect: 'latest-composable', - }), - ).resolves.toMatchInlineSnapshot(` - v Published initial schema. - i Available at http://localhost:8080/$organization/$project/production - `); - - await expect( - publish({ + })) ?? '', + ); + + expect(output).toEqual(expect.stringContaining('v Published initial schema.')); + expect(output).toEqual( + expect.stringContaining( + 'i Available at http://localhost:8080/$organization/$project/production', + ), + ); + + output = normalizeCliOutput( + (await publish({ sdl: /* GraphQL */ ` type Query { topProduct: Product @@ -231,16 +236,23 @@ describe('publish', () => { `, ...service, expect: 'latest-composable', - }), - ).resolves.toMatchInlineSnapshot(` - v Schema published - i Available at http://localhost:8080/$organization/$project/production/history/$version - `); + })) ?? '', + ); + + expect(output).toEqual( + expect.stringContaining(`v Schema published + `), + ); + expect(output).toEqual( + expect.stringContaining( + `i Available at http://localhost:8080/$organization/$project/production/history/$version`, + ), + ); }); }); describe('check', () => { - describe.each(cases)('%s', (caseName, ffs) => { + describe.concurrent.each(cases)('%s', (caseName, ffs) => { test.concurrent('accepted: composable, no breaking changes', async () => { const { publish, check } = await prepare(ffs); @@ -443,7 +455,7 @@ describe('publish', () => { }); describe('delete', () => { - describe.each(cases)('%s', (caseName, ffs) => { + describe.concurrent.each(cases)('%s', (caseName, ffs) => { test.concurrent('accepted: composable before and after', async () => { const cli = await prepare(ffs); @@ -517,7 +529,7 @@ describe('delete', () => { }); describe('other', () => { - describe.each(cases)('%s', (_, ffs) => { + describe.concurrent.each(cases)('%s', (_, ffs) => { test.concurrent( 'publish new schema when a field is moved from one service to another', async () => { diff --git a/package.json b/package.json index 60b670df09c..c57a3b4f92e 100644 --- a/package.json +++ b/package.json @@ -92,7 +92,7 @@ "tsx": "3.12.7", "turbo": "1.10.9", "typescript": "5.1.6", - "vitest": "0.31.1" + "vitest": "0.34.3" }, "husky": { "hooks": { diff --git a/packages/libraries/client/package.json b/packages/libraries/client/package.json index e13acb57af7..7c849382fbd 100644 --- a/packages/libraries/client/package.json +++ b/packages/libraries/client/package.json @@ -64,7 +64,7 @@ "@types/async-retry": "1.4.5", "graphql-yoga": "4.0.3", "nock": "13.3.2", - "vitest": "0.31.1" + "vitest": "0.34.3" }, "publishConfig": { "registry": "https://registry.npmjs.org", diff --git a/packages/migrations/src/actions/2023.09.07T14.14.14.native-fed-v2.ts b/packages/migrations/src/actions/2023.09.07T14.14.14.native-fed-v2.ts new file mode 100644 index 00000000000..2f73b8121a9 --- /dev/null +++ b/packages/migrations/src/actions/2023.09.07T14.14.14.native-fed-v2.ts @@ -0,0 +1,7 @@ +import { type MigrationExecutor } from '../pg-migrator'; + +export default { + name: '2023.09.07T14.14.14.native-fed-v2.ts', + run: ({ sql }) => + sql`ALTER TABLE "public"."projects" ADD COLUMN native_federation BOOLEAN DEFAULT FALSE;`, +} satisfies MigrationExecutor; diff --git a/packages/migrations/src/run-pg-migrations.ts b/packages/migrations/src/run-pg-migrations.ts index 7d587feeeb9..e999794a7d9 100644 --- a/packages/migrations/src/run-pg-migrations.ts +++ b/packages/migrations/src/run-pg-migrations.ts @@ -51,6 +51,7 @@ import migration_2023_06_06T11_26_04_schema_checks from './actions/2023.06.06T11 import migration_2023_07_10T11_26_04_schema_checks_manual_approval from './actions/2023.07.10T11.26.04.schema-checks-manual-approval'; import migration_2023_08_01T11_44_36_schema_checks_expires_at from './actions/2023.08.01T11.44.36.schema-checks-expires-at'; import migration_2023_09_01T09_54_00_zendesk_support from './actions/2023.09.01T09.54.00.zendesk-support'; +import migration_2023_09_07T14_14_14_native_fed_v2 from './actions/2023.09.07T14.14.14.native-fed-v2'; import { runMigrations } from './pg-migrator'; export const runPGMigrations = (args: { slonik: DatabasePool; runTo?: string }) => @@ -110,5 +111,6 @@ export const runPGMigrations = (args: { slonik: DatabasePool; runTo?: string }) migration_2023_07_27T11_44_36_graphql_endpoint, migration_2023_08_01T11_44_36_schema_checks_expires_at, migration_2023_09_01T09_54_00_zendesk_support, + migration_2023_09_07T14_14_14_native_fed_v2, ], }); diff --git a/packages/services/api/src/modules/lab/resolvers.ts b/packages/services/api/src/modules/lab/resolvers.ts index 7d7ace7c7e1..4716400a111 100644 --- a/packages/services/api/src/modules/lab/resolvers.ts +++ b/packages/services/api/src/modules/lab/resolvers.ts @@ -1,5 +1,6 @@ import { AuthManager } from '../auth/providers/auth-manager'; import { TargetAccessScope } from '../auth/providers/target-access'; +import { OrganizationManager } from '../organization/providers/organization-manager'; import { ProjectManager } from '../project/providers/project-manager'; import { ensureSDL, SchemaHelper } from '../schema/providers/schema-helper'; import { SchemaManager } from '../schema/providers/schema-manager'; @@ -35,7 +36,11 @@ export const resolvers: LabModule.Resolvers = { return null; } - const [schemas, { type, externalComposition }] = await Promise.all([ + const [ + schemas, + { type, externalComposition, nativeFederation, legacyRegistryModel }, + { featureFlags }, + ] = await Promise.all([ schemaManager.getSchemasOfVersion({ organization, project, @@ -46,6 +51,9 @@ export const resolvers: LabModule.Resolvers = { organization, project, }), + injector.get(OrganizationManager).getOrganization({ + organization, + }), ]); const orchestrator = schemaManager.matchOrchestrator(type); @@ -54,7 +62,20 @@ export const resolvers: LabModule.Resolvers = { const schema = await ensureSDL( orchestrator.composeAndValidate( schemas.map(s => helper.createSchemaObject(s)), - externalComposition, + { + external: externalComposition, + native: schemaManager.checkProjectNativeFederationSupport({ + project: { + id: project, + nativeFederation, + legacyRegistryModel, + }, + organization: { + id: organization, + featureFlags, + }, + }), + }, ), ); diff --git a/packages/services/api/src/modules/schema/providers/models/composite-legacy.ts b/packages/services/api/src/modules/schema/providers/models/composite-legacy.ts index cf2c7db27ab..5df3b753512 100644 --- a/packages/services/api/src/modules/schema/providers/models/composite-legacy.ts +++ b/packages/services/api/src/modules/schema/providers/models/composite-legacy.ts @@ -92,6 +92,7 @@ export class CompositeLegacyModel { project, schemas, baseSchema, + nativeFederation: false, }), this.checks.diff({ orchestrator, @@ -100,6 +101,7 @@ export class CompositeLegacyModel { selector, version: latestVersion, includeUrlChanges: false, + nativeFederation: false, }), ]); @@ -228,6 +230,7 @@ export class CompositeLegacyModel { project, schemas, baseSchema, + nativeFederation: false, }), this.checks.diff({ orchestrator, @@ -240,6 +243,7 @@ export class CompositeLegacyModel { schemas, version: latestVersion, includeUrlChanges: true, + nativeFederation: false, }), isFederation ? { diff --git a/packages/services/api/src/modules/schema/providers/models/single-legacy.ts b/packages/services/api/src/modules/schema/providers/models/single-legacy.ts index f71a4c72b3c..220200cb63a 100644 --- a/packages/services/api/src/modules/schema/providers/models/single-legacy.ts +++ b/packages/services/api/src/modules/schema/providers/models/single-legacy.ts @@ -82,6 +82,7 @@ export class SingleLegacyModel { project, schemas, baseSchema, + nativeFederation: false, }), this.checks.diff({ orchestrator: this.orchestrator, @@ -90,6 +91,7 @@ export class SingleLegacyModel { selector, version: latestVersion, includeUrlChanges: false, + nativeFederation: false, }), ]); @@ -176,6 +178,7 @@ export class SingleLegacyModel { } : incoming, ], + nativeFederation: false, }), this.checks.diff({ orchestrator: this.orchestrator, @@ -188,6 +191,7 @@ export class SingleLegacyModel { schemas, version: latestVersion, includeUrlChanges: false, + nativeFederation: false, }), this.checks.metadata(incoming, latestVersion ? latestVersion.schemas[0] : null), ]); 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 0b525ba71c2..553c3bfb594 100644 --- a/packages/services/api/src/modules/schema/providers/models/single.ts +++ b/packages/services/api/src/modules/schema/providers/models/single.ts @@ -89,6 +89,7 @@ export class SingleModel { project, schemas, baseSchema, + nativeFederation: false, }), this.checks.diff({ orchestrator: this.orchestrator, @@ -97,6 +98,7 @@ export class SingleModel { selector, version: compareToLatest ? latest : latestComposable, includeUrlChanges: false, + nativeFederation: false, }), this.checks.policyCheck({ orchestrator: this.orchestrator, @@ -105,6 +107,7 @@ export class SingleModel { schemas, modifiedSdl: input.sdl, baseSchema, + nativeFederation: false, }), ]); @@ -200,6 +203,7 @@ export class SingleModel { } : incoming, ], + nativeFederation: false, }), this.checks.metadata(incoming, latestVersion ? latestVersion.schemas[0] : null), this.checks.diff({ @@ -213,6 +217,7 @@ export class SingleModel { }, version: compareToLatest ? latestVersion : latestComposable, includeUrlChanges: false, + nativeFederation: false, }), ]); diff --git a/packages/services/api/src/modules/schema/providers/orchestrators/federation.ts b/packages/services/api/src/modules/schema/providers/orchestrators/federation.ts index c68124bd4dd..871622ef506 100644 --- a/packages/services/api/src/modules/schema/providers/orchestrators/federation.ts +++ b/packages/services/api/src/modules/schema/providers/orchestrators/federation.ts @@ -60,7 +60,13 @@ export class FederationOrchestrator implements Orchestrator { } @sentry('FederationOrchestrator.composeAndValidate') - async composeAndValidate(schemas: SchemaObject[], external: Project['externalComposition']) { + async composeAndValidate( + schemas: SchemaObject[], + config: { + external: Project['externalComposition']; + native: boolean; + }, + ) { this.logger.debug('Composing and Validating Federated Schemas'); const result = await this.schemaService.composeAndValidate.mutate({ type: 'federation', @@ -69,7 +75,8 @@ export class FederationOrchestrator implements Orchestrator { source: s.source, url: s.url ?? null, })), - external: this.createConfig(external), + external: this.createConfig(config.external), + native: config.native, }); return result; diff --git a/packages/services/api/src/modules/schema/providers/orchestrators/single.ts b/packages/services/api/src/modules/schema/providers/orchestrators/single.ts index b0b37901abf..b18b56a9310 100644 --- a/packages/services/api/src/modules/schema/providers/orchestrators/single.ts +++ b/packages/services/api/src/modules/schema/providers/orchestrators/single.ts @@ -52,7 +52,6 @@ export class SingleOrchestrator implements Orchestrator { raw: s.raw, source: s.source, })), - external: null, }); return result; diff --git a/packages/services/api/src/modules/schema/providers/orchestrators/stitching.ts b/packages/services/api/src/modules/schema/providers/orchestrators/stitching.ts index b279f658e7c..93624e53666 100644 --- a/packages/services/api/src/modules/schema/providers/orchestrators/stitching.ts +++ b/packages/services/api/src/modules/schema/providers/orchestrators/stitching.ts @@ -46,7 +46,6 @@ export class StitchingOrchestrator implements Orchestrator { source: s.source, url: s.url ?? null, })), - external: null, }); return result; diff --git a/packages/services/api/src/modules/schema/providers/registry-checks.ts b/packages/services/api/src/modules/schema/providers/registry-checks.ts index d3b3b7a766f..2fd5e2cbab8 100644 --- a/packages/services/api/src/modules/schema/providers/registry-checks.ts +++ b/packages/services/api/src/modules/schema/providers/registry-checks.ts @@ -111,15 +111,20 @@ export class RegistryChecks { project, schemas, baseSchema, + nativeFederation, }: { orchestrator: Orchestrator; project: Project; schemas: Schemas; baseSchema: string | null; + nativeFederation: boolean; }) { const result = await orchestrator.composeAndValidate( extendWithBase(schemas, baseSchema).map(s => this.helper.createSchemaObject(s)), - project.externalComposition, + { + external: project.externalComposition, + native: nativeFederation, + }, ); const validationErrors = result.errors; @@ -161,6 +166,7 @@ export class RegistryChecks { selector, modifiedSdl, baseSchema, + nativeFederation, }: { orchestrator: Orchestrator; schemas: [SingleSchema] | PushedCompositeSchema[]; @@ -172,10 +178,14 @@ export class RegistryChecks { target: string; }; baseSchema: string | null; + nativeFederation: boolean; }) { const result = await orchestrator.composeAndValidate( extendWithBase(schemas, baseSchema).map(s => this.helper.createSchemaObject(s)), - project.externalComposition, + { + external: project.externalComposition, + native: nativeFederation, + }, ); if (result.sdl == null) { @@ -219,6 +229,7 @@ export class RegistryChecks { version, selector, includeUrlChanges, + nativeFederation, }: { orchestrator: Orchestrator; project: Project; @@ -230,6 +241,7 @@ export class RegistryChecks { target: string; }; includeUrlChanges: boolean; + nativeFederation: boolean; }) { if (!version || version.schemas.length === 0) { this.logger.debug('Skipping diff check, no existing version'); @@ -241,11 +253,17 @@ export class RegistryChecks { const [existingSchemaResult, incomingSchemaResult] = await Promise.all([ orchestrator.composeAndValidate( version.schemas.map(s => this.helper.createSchemaObject(s)), - project.externalComposition, + { + external: project.externalComposition, + native: nativeFederation, + }, ), orchestrator.composeAndValidate( schemas.map(s => this.helper.createSchemaObject(s)), - project.externalComposition, + { + external: project.externalComposition, + native: nativeFederation, + }, ), ]); 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 32a5f005724..c70a89743ac 100644 --- a/packages/services/api/src/modules/schema/providers/schema-manager.ts +++ b/packages/services/api/src/modules/schema/providers/schema-manager.ts @@ -5,7 +5,13 @@ import { z } from 'zod'; import { Change } from '@graphql-inspector/core'; import type { SchemaCheck, SchemaCompositionError } from '@hive/storage'; import { RegistryModel } from '../../../__generated__/types'; -import { DateRange, Orchestrator, ProjectType } from '../../../shared/entities'; +import { + DateRange, + Orchestrator, + Organization, + Project, + ProjectType, +} from '../../../shared/entities'; import { HiveError } from '../../../shared/errors'; import { atomic, stringifySelector } from '../../../shared/helpers'; import { SchemaVersion } from '../../../shared/mappers'; @@ -363,10 +369,15 @@ export class SchemaManager { scope: ProjectAccessScope.SETTINGS, }); - const project = await this.storage.getProject({ - organization: selector.organizationId, - project: selector.projectId, - }); + const [project, organization] = await Promise.all([ + this.storage.getProject({ + organization: selector.organizationId, + project: selector.projectId, + }), + this.storage.getOrganization({ + organization: selector.organizationId, + }), + ]); if (project.type !== ProjectType.FEDERATION) { throw new HiveError( @@ -390,7 +401,13 @@ export class SchemaManager { url: null, }, ], - project.externalComposition, + { + external: project.externalComposition, + native: this.checkProjectNativeFederationSupport({ + project, + organization, + }), + }, ); if (errors.length > 0) { @@ -816,6 +833,40 @@ export class SchemaManager { organization: organization.id, }; } + + checkProjectNativeFederationSupport(input: { + project: Pick; + organization: Pick; + }) { + if (input.project.nativeFederation === false) { + return false; + } + + if (input.project.legacyRegistryModel === true) { + this.logger.warn( + 'Project is using legacy registry model, ignoring native Federation support (organization=%s, project=%s)', + input.organization.id, + input.project.id, + ); + return false; + } + + if (input.organization.featureFlags.compareToPreviousComposableVersion === false) { + this.logger.warn( + 'Organization has compareToPreviousComposableVersion FF disabled, ignoring native Federation support (organization=%s, project=%s)', + input.organization.id, + input.project.id, + ); + return false; + } + + this.logger.debug( + 'Native Federation support available (organization=%s, project=%s)', + input.organization.id, + input.project.id, + ); + return true; + } } /** 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 9be26cef48a..a3039928244 100644 --- a/packages/services/api/src/modules/schema/providers/schema-publisher.ts +++ b/packages/services/api/src/modules/schema/providers/schema-publisher.ts @@ -358,7 +358,13 @@ export class SchemaPublisher { const result = await orchestrator.composeAndValidate( latestVersion.schemas.map(s => this.helper.createSchemaObject(s)), - project.externalComposition, + { + external: project.externalComposition, + native: this.schemaManager.checkProjectNativeFederationSupport({ + project, + organization, + }), + }, ); if (result.sdl == null) { @@ -522,7 +528,10 @@ export class SchemaPublisher { // if it is the latest version, we should update the CDN if (latestVersion.id === updateResult.id) { this.logger.info('Version is now promoted to latest valid (version=%s)', latestVersion.id); - const [project, target, schemas] = await Promise.all([ + const [organization, project, target, schemas] = await Promise.all([ + this.organizationManager.getOrganization({ + organization: input.organization, + }), this.projectManager.getProject({ organization: input.organization, project: input.project, @@ -543,10 +552,13 @@ export class SchemaPublisher { const orchestrator = this.schemaManager.matchOrchestrator(project.type); const schemaObjects = schemas.map(s => this.helper.createSchemaObject(s)); - const compositionResult = await orchestrator.composeAndValidate( - schemaObjects, - project.externalComposition, - ); + const compositionResult = await orchestrator.composeAndValidate(schemaObjects, { + external: project.externalComposition, + native: this.schemaManager.checkProjectNativeFederationSupport({ + project, + organization, + }), + }); this.logger.info( 'Deploying version to CDN (reason="status_change" version=%s)', diff --git a/packages/services/api/src/modules/schema/resolvers.ts b/packages/services/api/src/modules/schema/resolvers.ts index 9d8bc4b7cb3..24c4a277614 100644 --- a/packages/services/api/src/modules/schema/resolvers.ts +++ b/packages/services/api/src/modules/schema/resolvers.ts @@ -417,15 +417,13 @@ export const resolvers: SchemaModule.Resolvers = { const getBeforeSchemaSDL = async () => { const orchestrator = schemaManager.matchOrchestrator(project.type); - const { schemas: schemasBefore } = await injector - .get(SchemaManager) - .getSchemasOfPreviousVersion({ - organization: organizationId, - project: projectId, - target: targetId, - version: selector.version, - onlyComposable: organization.featureFlags.compareToPreviousComposableVersion === true, - }); + const { schemas: schemasBefore } = await schemaManager.getSchemasOfPreviousVersion({ + organization: organizationId, + project: projectId, + target: targetId, + version: selector.version, + onlyComposable: organization.featureFlags.compareToPreviousComposableVersion === true, + }); if (schemasBefore.length === 0) { return null; @@ -433,7 +431,13 @@ export const resolvers: SchemaModule.Resolvers = { const { raw } = await ensureSDL( orchestrator.composeAndValidate( schemasBefore.map(s => helper.createSchemaObject(s)), - project.externalComposition, + { + external: project.externalComposition, + native: schemaManager.checkProjectNativeFederationSupport({ + project, + organization, + }), + }, ), ); @@ -496,14 +500,26 @@ export const resolvers: SchemaModule.Resolvers = { ? ensureSDL( orchestrator.composeAndValidate( schemasBefore.map(s => helper.createSchemaObject(s)), - project.externalComposition, + { + external: project.externalComposition, + native: schemaManager.checkProjectNativeFederationSupport({ + project, + organization, + }), + }, ), ) : null, ensureSDL( orchestrator.composeAndValidate( schemasAfter.map(s => helper.createSchemaObject(s)), - project.externalComposition, + { + external: project.externalComposition, + native: schemaManager.checkProjectNativeFederationSupport({ + project, + organization, + }), + }, ), organization.featureFlags.compareToPreviousComposableVersion === true ? // Do not show schema changes if the new version is not composable @@ -779,7 +795,7 @@ export const resolvers: SchemaModule.Resolvers = { async errors(version, _, { injector }) { const schemaManager = injector.get(SchemaManager); const schemaHelper = injector.get(SchemaHelper); - const [schemas, project] = await Promise.all([ + const [schemas, project, organization] = await Promise.all([ schemaManager.getMaybeSchemasOfVersion({ version: version.id, organization: version.organization, @@ -790,6 +806,9 @@ export const resolvers: SchemaModule.Resolvers = { organization: version.organization, project: version.project, }), + injector.get(OrganizationManager).getOrganization({ + organization: version.organization, + }), ]); if (schemas.length === 0) { @@ -799,16 +818,27 @@ export const resolvers: SchemaModule.Resolvers = { const orchestrator = schemaManager.matchOrchestrator(project.type); const validation = await orchestrator.composeAndValidate( schemas.map(s => schemaHelper.createSchemaObject(s)), - project.externalComposition, + { + external: project.externalComposition, + native: schemaManager.checkProjectNativeFederationSupport({ + project, + organization, + }), + }, ); return validation.errors; }, async supergraph(version, _, { injector }) { - const project = await injector.get(ProjectManager).getProject({ - organization: version.organization, - project: version.project, - }); + const [project, organization] = await Promise.all([ + injector.get(ProjectManager).getProject({ + organization: version.organization, + project: version.project, + }), + injector.get(OrganizationManager).getOrganization({ + organization: version.organization, + }), + ]); if (project.type !== ProjectType.FEDERATION) { return null; @@ -837,15 +867,26 @@ export const resolvers: SchemaModule.Resolvers = { return orchestrator .composeAndValidate( schemas.map(s => helper.createSchemaObject(s)), - project.externalComposition, + { + external: project.externalComposition, + native: schemaManager.checkProjectNativeFederationSupport({ + project, + organization, + }), + }, ) .then(r => r.supergraph); }, async sdl(version, _, { injector }) { - const project = await injector.get(ProjectManager).getProject({ - organization: version.organization, - project: version.project, - }); + const [project, organization] = await Promise.all([ + injector.get(ProjectManager).getProject({ + organization: version.organization, + project: version.project, + }), + injector.get(OrganizationManager).getOrganization({ + organization: version.organization, + }), + ]); const schemaManager = injector.get(SchemaManager); const orchestrator = schemaManager.matchOrchestrator(project.type); @@ -867,7 +908,13 @@ export const resolvers: SchemaModule.Resolvers = { await ensureSDL( orchestrator.composeAndValidate( schemas.map(s => helper.createSchemaObject(s)), - project.externalComposition, + { + external: project.externalComposition, + native: schemaManager.checkProjectNativeFederationSupport({ + project, + organization, + }), + }, ), ) ).raw; @@ -876,10 +923,15 @@ export const resolvers: SchemaModule.Resolvers = { return version.baseSchema || null; }, async explorer(version, { usage }, { injector }) { - const project = await injector.get(ProjectManager).getProject({ - organization: version.organization, - project: version.project, - }); + const [project, organization] = await Promise.all([ + injector.get(ProjectManager).getProject({ + organization: version.organization, + project: version.project, + }), + injector.get(OrganizationManager).getOrganization({ + organization: version.organization, + }), + ]); const schemaManager = injector.get(SchemaManager); const orchestrator = schemaManager.matchOrchestrator(project.type); @@ -902,7 +954,13 @@ export const resolvers: SchemaModule.Resolvers = { const result = await orchestrator.composeAndValidate( schemas.map(s => helper.createSchemaObject(s)), - project.externalComposition, + { + external: project.externalComposition, + native: schemaManager.checkProjectNativeFederationSupport({ + project, + organization, + }), + }, ); if (result.supergraph) { @@ -930,7 +988,13 @@ export const resolvers: SchemaModule.Resolvers = { const schema = await ensureSDL( orchestrator.composeAndValidate( schemas.map(s => helper.createSchemaObject(s)), - project.externalComposition, + { + external: project.externalComposition, + native: schemaManager.checkProjectNativeFederationSupport({ + project, + organization, + }), + }, ), ); diff --git a/packages/services/api/src/shared/entities.ts b/packages/services/api/src/shared/entities.ts index f9894751b80..2361d326d92 100644 --- a/packages/services/api/src/shared/entities.ts +++ b/packages/services/api/src/shared/entities.ts @@ -268,6 +268,7 @@ export interface Project { endpoint?: string | null; encryptedSecret?: string | null; }; + nativeFederation: boolean; } export interface Target { @@ -333,7 +334,10 @@ export interface ComposeAndValidateResult { export interface Orchestrator { composeAndValidate( schemas: SchemaObject[], - config: Project['externalComposition'], + config: { + external: Project['externalComposition'] | null; + native: boolean; + }, ): Promise; } diff --git a/packages/services/schema/package.json b/packages/services/schema/package.json index d9c23c38f59..7a6cf48b045 100644 --- a/packages/services/schema/package.json +++ b/packages/services/schema/package.json @@ -16,6 +16,7 @@ "@graphql-tools/stitching-directives": "3.0.0", "@hive/service-common": "workspace:*", "@sentry/node": "7.59.3", + "@theguild/federation-composition": "0.0.0-alpha-20230916192321-bb37b43", "@trpc/server": "10.31.0", "@types/async-retry": "1.4.5", "@types/ioredis-mock": "8.2.2", diff --git a/packages/services/schema/src/api.ts b/packages/services/schema/src/api.ts index 502aa6b9ca6..d6e71a8dc39 100644 --- a/packages/services/schema/src/api.ts +++ b/packages/services/schema/src/api.ts @@ -34,48 +34,53 @@ const EXTERNAL_VALIDATION = z export const schemaBuilderApiRouter = t.router({ composeAndValidate: procedure .input( - z.union([ - z - .object({ - type: z.literal('single'), - schemas: z.array( - z - .object({ - raw: z.string().min(1), - source: z.string().min(1), - }) - .required(), - ), - external: EXTERNAL_VALIDATION, - }) - .required(), - z - .object({ - type: z.union([z.literal('federation'), z.literal('stitching')]), - schemas: z.array( - z - .object({ - raw: z.string().min(1), - source: z.string().min(1), - url: z.string().nullish(), - }) - .required(), - ), - external: EXTERNAL_VALIDATION, - }) - .required(), + z.discriminatedUnion('type', [ + z.object({ + type: z.literal('single'), + schemas: z.array( + z.object({ + raw: z.string().min(1), + source: z.string().min(1), + }), + ), + }), + z.object({ + type: z.literal('federation'), + schemas: z.array( + z + .object({ + raw: z.string().min(1), + source: z.string().min(1), + url: z.string().nullish(), + }) + .required(), + ), + external: EXTERNAL_VALIDATION, + native: z.boolean().optional(), + }), + z.object({ + type: z.literal('stitching'), + schemas: z.array( + z.object({ + raw: z.string().min(1), + source: z.string().min(1), + url: z.string().nullish(), + }), + ), + }), ]), ) .mutation(async ({ ctx, input }) => { composeAndValidateCounter.inc({ type: input.type }); return await pickOrchestrator(input.type, ctx.cache, ctx.req, ctx.decrypt).composeAndValidate( input.schemas, - input.external + 'external' in input && input.external ? { ...input.external, broker: ctx.broker, } : null, + 'native' in input && input.native ? true : false, ); }), }); diff --git a/packages/services/schema/src/orchestrators.ts b/packages/services/schema/src/orchestrators.ts index 5d47a3fb340..8a8e46041ca 100644 --- a/packages/services/schema/src/orchestrators.ts +++ b/packages/services/schema/src/orchestrators.ts @@ -20,6 +20,11 @@ import type { ErrorCode } from '@graphql-hive/external-composition'; import { stitchSchemas } from '@graphql-tools/stitch'; import { stitchingDirectives } from '@graphql-tools/stitching-directives'; import type { FastifyLoggerInstance } from '@hive/service-common'; +import { + composeServices as nativeComposeServices, + compositionHasErrors as nativeCompositionHasErrors, + stripFederationFromSupergraph, +} from '@the-guild-org/federation-composition'; import type { Cache } from './cache'; import type { ComposeAndValidateInput, @@ -180,6 +185,7 @@ interface Orchestrator { composeAndValidate( input: ComposeAndValidateInput, external: ExternalComposition, + native: boolean, ): Promise; } @@ -311,6 +317,62 @@ async function callExternalService( } } +function composeFederationV1( + subgraphs: Array<{ + typeDefs: DocumentNode; + name: string; + url: string | undefined; + }>, +): CompositionSuccess | CompositionFailure { + const result = composeAndValidate(subgraphs); + + if (compositionHasErrors(result)) { + return { + type: 'failure', + result: { + errors: result.errors.map(errorWithPossibleCode), + raw: result.schema ? printSchema(result.schema) : undefined, + }, + }; + } + + return { + type: 'success', + result: { + supergraphSdl: result.supergraphSdl, + raw: printSchema(result.schema), + }, + }; +} + +function composeFederationV2( + subgraphs: Array<{ + typeDefs: DocumentNode; + name: string; + url: string | undefined; + }>, +): CompositionSuccess | CompositionFailure { + const result = nativeComposeServices(subgraphs); + + if (nativeCompositionHasErrors(result)) { + return { + type: 'failure', + result: { + errors: result.errors.map(errorWithPossibleCode), + raw: undefined, + }, + }; + } + + return { + type: 'success', + result: { + supergraphSdl: result.supergraphSdl, + raw: print(stripFederationFromSupergraph(parse(result.supergraphSdl))), + }, + }; +} + const createFederation: ( cache: Cache, logger: FastifyLoggerInstance, @@ -321,9 +383,24 @@ const createFederation: ( { schemas: ComposeAndValidateInput; external: ExternalComposition; + native: boolean; }, CompositionSuccess | CompositionFailure - >('federation', async ({ schemas, external }) => { + >('federation', async ({ schemas, external, native }) => { + const subgraphs = schemas.map(schema => { + return { + typeDefs: trimDescriptions(parse(schema.raw)), + name: schema.source, + url: 'url' in schema && typeof schema.url === 'string' ? schema.url : undefined, + }; + }); + + // Federation v2 + if (native) { + logger.debug('Using built-in Federation v2 composition service (schemas=%s)', schemas.length); + return composeFederationV2(subgraphs); + } + if (external) { logger.debug( 'Using external composition service (url=%s, schemas=%s)', @@ -331,11 +408,11 @@ const createFederation: ( schemas.length, ); const body = JSON.stringify( - schemas.map(schema => { + subgraphs.map(schema => { return { - sdl: print(trimDescriptions(parse(schema.raw))), - name: schema.source, - url: 'url' in schema && typeof schema.url === 'string' ? schema.url : undefined, + sdl: print(schema.typeDefs), + name: schema.name, + url: schema.url, }; }), ); @@ -388,41 +465,15 @@ const createFederation: ( return parseResult.data; } - logger.debug('Using built-in composition service (schemas=%s)', schemas.length); - - const result = composeAndValidate( - schemas.map(schema => { - return { - typeDefs: trimDescriptions(parse(schema.raw)), - name: schema.source, - url: 'url' in schema && typeof schema.url === 'string' ? schema.url : undefined, - }; - }), - ); - - if (compositionHasErrors(result)) { - return { - type: 'failure', - result: { - errors: result.errors.map(errorWithPossibleCode), - raw: result.schema ? printSchema(result.schema) : undefined, - }, - }; - } - - return { - type: 'success', - result: { - supergraphSdl: result.supergraphSdl, - raw: printSchema(result.schema), - }, - }; + // Federation v1 + logger.debug('Using built-in Federation v1 composition service (schemas=%s)', schemas.length); + return composeFederationV1(subgraphs); }); return { - async composeAndValidate(schemas, external) { + async composeAndValidate(schemas, external, native) { try { - const composed = await compose({ schemas, external }); + const composed = await compose({ schemas, external, native }); return { errors: composed.type === 'failure' ? composed.result.errors : [], diff --git a/packages/services/storage/src/db/types.ts b/packages/services/storage/src/db/types.ts index 047a4d4ecfc..d73b7a9af22 100644 --- a/packages/services/storage/src/db/types.ts +++ b/packages/services/storage/src/db/types.ts @@ -161,6 +161,7 @@ export interface projects { id: string; legacy_registry_model: boolean; name: string; + native_federation: boolean | null; org_id: string; type: string; validation_url: string | null; diff --git a/packages/services/storage/src/index.ts b/packages/services/storage/src/index.ts index eff949af42d..9864e4b5261 100644 --- a/packages/services/storage/src/index.ts +++ b/packages/services/storage/src/index.ts @@ -211,6 +211,7 @@ export async function createStorage(connection: string, maximumPoolSize: number) endpoint: project.external_composition_endpoint, encryptedSecret: project.external_composition_secret, }, + nativeFederation: project.native_federation === true, }; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 905f2dba1ea..9d77d2b9c9b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -176,8 +176,8 @@ importers: specifier: 5.1.6 version: 5.1.6 vitest: - specifier: 0.31.1 - version: 0.31.1 + specifier: 0.34.3 + version: 0.34.3(@vitest/ui@0.34.3) deployment: devDependencies: @@ -265,6 +265,9 @@ importers: '@types/ioredis': specifier: 4.28.10 version: 4.28.10 + '@vitest/ui': + specifier: 0.34.3 + version: 0.34.3(vitest@0.34.3) '@whatwg-node/fetch': specifier: 0.9.9 version: 0.9.9 @@ -299,8 +302,8 @@ importers: specifier: 2.5.3 version: 2.5.3 vitest: - specifier: 0.31.1 - version: 0.31.1 + specifier: 0.34.3 + version: 0.34.3(@vitest/ui@0.34.3) zod: specifier: 3.21.4 version: 3.21.4 @@ -434,8 +437,8 @@ importers: specifier: 13.3.2 version: 13.3.2 vitest: - specifier: 0.31.1 - version: 0.31.1 + specifier: 0.34.3 + version: 0.34.3(@vitest/ui@0.34.3) publishDirectory: dist packages/libraries/core: @@ -942,6 +945,9 @@ importers: '@sentry/node': specifier: 7.59.3 version: 7.59.3 + '@theguild/federation-composition': + specifier: 0.0.0-alpha-20230916192321-bb37b43 + version: 0.0.0-alpha-20230916192321-bb37b43(graphql@16.6.0) '@trpc/server': specifier: 10.31.0 version: 10.31.0 @@ -7675,11 +7681,11 @@ packages: engines: {node: '>=8'} dev: true - /@jest/schemas@29.4.3: - resolution: {integrity: sha512-VLYKXQmtmuEz6IxJsrZwzG9NvtkQsWNnWMsKxqWNu3+CnfzJQhp0WDDKWLVV9hLKr0l3SLLFRqcYHjhtyuDVxg==} + /@jest/schemas@29.6.3: + resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@sinclair/typebox': 0.25.21 + '@sinclair/typebox': 0.27.8 dev: true /@jest/transform@29.4.3: @@ -7720,7 +7726,7 @@ packages: resolution: {integrity: sha512-bPYfw8V65v17m2Od1cv44FH+SiKW7w2Xu7trhcdTLUmSv85rfKsP+qXSjO4KGJr4dtPSzl/gvslZBXctf1qGEA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/schemas': 29.4.3 + '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.4 '@types/istanbul-reports': 3.0.1 '@types/node': 18.16.19 @@ -7736,14 +7742,14 @@ packages: engines: {node: '>=6.0.0'} dependencies: '@jridgewell/set-array': 1.1.2 - '@jridgewell/sourcemap-codec': 1.4.14 + '@jridgewell/sourcemap-codec': 1.4.15 /@jridgewell/gen-mapping@0.3.2: resolution: {integrity: sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==} engines: {node: '>=6.0.0'} dependencies: '@jridgewell/set-array': 1.1.2 - '@jridgewell/sourcemap-codec': 1.4.14 + '@jridgewell/sourcemap-codec': 1.4.15 '@jridgewell/trace-mapping': 0.3.18 /@jridgewell/resolve-uri@3.1.0: @@ -7763,6 +7769,9 @@ packages: /@jridgewell/sourcemap-codec@1.4.14: resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==} + /@jridgewell/sourcemap-codec@1.4.15: + resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + /@jridgewell/trace-mapping@0.3.18: resolution: {integrity: sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==} dependencies: @@ -7773,7 +7782,7 @@ packages: resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} dependencies: '@jridgewell/resolve-uri': 3.1.0 - '@jridgewell/sourcemap-codec': 1.4.14 + '@jridgewell/sourcemap-codec': 1.4.15 /@juggle/resize-observer@3.4.0: resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==} @@ -10667,8 +10676,8 @@ packages: engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} dev: true - /@sinclair/typebox@0.25.21: - resolution: {integrity: sha512-gFukHN4t8K4+wVC+ECqeqwzBDeFeTzBXroBTqE6vcWrQGbEUpHO7LYdG0f4xnvYq4VOEwITSlHlp0JBAIFMS/g==} + /@sinclair/typebox@0.27.8: + resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} dev: true /@sindresorhus/is@4.6.0: @@ -12118,6 +12127,17 @@ packages: - typescript dev: true + /@theguild/federation-composition@0.0.0-alpha-20230916192321-bb37b43(graphql@16.6.0): + resolution: {integrity: sha512-oWChelsSZkTnFYl/0/O10AbmxSqfcCJSNXZvtyMwzC45m69UqgOwSXxwUL1BMTCOrgCTF/gRauRoDQCaNDffbw==} + engines: {node: '>=18'} + peerDependencies: + graphql: ^16.0.0 + dependencies: + constant-case: 3.0.4 + graphql: 16.6.0 + json5: 2.2.3 + dev: true + /@theguild/prettier-config@1.2.0(prettier@2.8.8): resolution: {integrity: sha512-7zws6rvfO63xPEExM8A8/hFmFnleS/oHEov+iNbWe4QddO5e/zGmm69LuoptsrB9GAPsaxxOAUf8tn23V/1ljA==} peerDependencies: @@ -12976,43 +12996,57 @@ packages: - graphql dev: false - /@vitest/expect@0.31.1: - resolution: {integrity: sha512-BV1LyNvhnX+eNYzJxlHIGPWZpwJFZaCcOIzp2CNG0P+bbetenTupk6EO0LANm4QFt0TTit+yqx7Rxd1qxi/SQA==} + /@vitest/expect@0.34.3: + resolution: {integrity: sha512-F8MTXZUYRBVsYL1uoIft1HHWhwDbSzwAU9Zgh8S6WFC3YgVb4AnFV2GXO3P5Em8FjEYaZtTnQYoNwwBrlOMXgg==} dependencies: - '@vitest/spy': 0.31.1 - '@vitest/utils': 0.31.1 + '@vitest/spy': 0.34.3 + '@vitest/utils': 0.34.3 chai: 4.3.7 dev: true - /@vitest/runner@0.31.1: - resolution: {integrity: sha512-imWuc82ngOtxdCUpXwtEzZIuc1KMr+VlQ3Ondph45VhWoQWit5yvG/fFcldbnCi8DUuFi+NmNx5ehMUw/cGLUw==} + /@vitest/runner@0.34.3: + resolution: {integrity: sha512-lYNq7N3vR57VMKMPLVvmJoiN4bqwzZ1euTW+XXYH5kzr3W/+xQG3b41xJn9ChJ3AhYOSoweu974S1V3qDcFESA==} dependencies: - '@vitest/utils': 0.31.1 - concordance: 5.0.4 + '@vitest/utils': 0.34.3 p-limit: 4.0.0 - pathe: 1.1.0 + pathe: 1.1.1 dev: true - /@vitest/snapshot@0.31.1: - resolution: {integrity: sha512-L3w5uU9bMe6asrNzJ8WZzN+jUTX4KSgCinEJPXyny0o90fG4FPQMV0OWsq7vrCWfQlAilMjDnOF9nP8lidsJ+g==} + /@vitest/snapshot@0.34.3: + resolution: {integrity: sha512-QyPaE15DQwbnIBp/yNJ8lbvXTZxS00kRly0kfFgAD5EYmCbYcA+1EEyRalc93M0gosL/xHeg3lKAClIXYpmUiQ==} dependencies: - magic-string: 0.30.0 - pathe: 1.1.0 - pretty-format: 27.5.1 + magic-string: 0.30.3 + pathe: 1.1.1 + pretty-format: 29.6.3 dev: true - /@vitest/spy@0.31.1: - resolution: {integrity: sha512-1cTpt2m9mdo3hRLDyCG2hDQvRrePTDgEJBFQQNz1ydHHZy03EiA6EpFxY+7ODaY7vMRCie+WlFZBZ0/dQWyssQ==} + /@vitest/spy@0.34.3: + resolution: {integrity: sha512-N1V0RFQ6AI7CPgzBq9kzjRdPIgThC340DGjdKdPSE8r86aUSmeliTUgkTqLSgtEwWWsGfBQ+UetZWhK0BgJmkQ==} dependencies: - tinyspy: 2.1.0 + tinyspy: 2.1.1 dev: true - /@vitest/utils@0.31.1: - resolution: {integrity: sha512-yFyRD5ilwojsZfo3E0BnH72pSVSuLg2356cN1tCEe/0RtDzxTPYwOomIC+eQbot7m6DRy4tPZw+09mB7NkbMmA==} + /@vitest/ui@0.34.3(vitest@0.34.3): + resolution: {integrity: sha512-iNcOQ0xML9znOReiwpKJrTLSj5zFxmveD3VCxIJNqnsaMYpONSbSiiJLC1Y1dYlkmiHylp+ElNcUZYIMWdxRvA==} + peerDependencies: + vitest: '>=0.30.1 <1' + dependencies: + '@vitest/utils': 0.34.3 + fast-glob: 3.3.1 + fflate: 0.8.0 + flatted: 3.2.7 + pathe: 1.1.1 + picocolors: 1.0.0 + sirv: 2.0.3 + vitest: 0.34.3(@vitest/ui@0.34.3) + dev: true + + /@vitest/utils@0.34.3: + resolution: {integrity: sha512-kiSnzLG6m/tiT0XEl4U2H8JDBjFtwVlaE8I3QfGiMFR0QvnRDfYfdP3YvTBWM/6iJDAyaPY6yVQiCTUc7ZzTHA==} dependencies: - concordance: 5.0.4 + diff-sequences: 29.6.3 loupe: 2.3.6 - pretty-format: 27.5.1 + pretty-format: 29.6.3 dev: true /@webassemblyjs/ast@1.11.1: @@ -13352,12 +13386,12 @@ packages: mime-types: 2.1.35 negotiator: 0.6.3 - /acorn-import-assertions@1.8.0(acorn@8.8.2): + /acorn-import-assertions@1.8.0(acorn@8.10.0): resolution: {integrity: sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==} peerDependencies: acorn: ^8 dependencies: - acorn: 8.8.2 + acorn: 8.10.0 /acorn-jsx@5.3.2(acorn@7.4.1): resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} @@ -13367,12 +13401,12 @@ packages: acorn: 7.4.1 dev: true - /acorn-jsx@5.3.2(acorn@8.8.2): + /acorn-jsx@5.3.2(acorn@8.10.0): resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 dependencies: - acorn: 8.8.2 + acorn: 8.10.0 /acorn-walk@7.2.0: resolution: {integrity: sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==} @@ -13395,13 +13429,13 @@ packages: hasBin: true dev: true - /acorn@8.8.0: - resolution: {integrity: sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==} + /acorn@8.10.0: + resolution: {integrity: sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==} engines: {node: '>=0.4.0'} hasBin: true - /acorn@8.8.2: - resolution: {integrity: sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==} + /acorn@8.8.0: + resolution: {integrity: sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==} engines: {node: '>=0.4.0'} hasBin: true @@ -14320,10 +14354,6 @@ packages: /bluebird@3.7.2: resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} - /blueimp-md5@2.19.0: - resolution: {integrity: sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==} - dev: true - /bn.js@4.12.0: resolution: {integrity: sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==} @@ -15519,20 +15549,6 @@ packages: typedarray: 0.0.6 dev: true - /concordance@5.0.4: - resolution: {integrity: sha512-OAcsnTEYu1ARJqWVGwf4zh4JDfHZEaSNlNccFmt8YjB2l/n19/PF2viLINHc57vO4FKIAFl2FWASIGZZWZ2Kxw==} - engines: {node: '>=10.18.0 <11 || >=12.14.0 <13 || >=14'} - dependencies: - date-time: 3.1.0 - esutils: 2.0.3 - fast-diff: 1.2.0 - js-string-escape: 1.0.1 - lodash: 4.17.21 - md5-hex: 3.0.1 - semver: 7.5.4 - well-known-symbols: 2.0.0 - dev: true - /concurrently@7.6.0: resolution: {integrity: sha512-BKtRgvcJGeZ4XttiDiNcFiRlxoAeZOseqUvyYRUp/Vtd+9p1ULmeoSqGsDA+2ivdeDFpqrJvGvmI+StKfKl5hw==} engines: {node: ^12.20.0 || ^14.13.0 || >=16.0.0} @@ -16371,13 +16387,6 @@ packages: dependencies: '@babel/runtime': 7.21.0 - /date-time@3.1.0: - resolution: {integrity: sha512-uqCUKXE5q1PNBXjPqvwhwJf9SwMoAHBgWJ6DcrnS5o+W2JOiIILl0JEdVD8SGujrNS02GGxgwAg2PN2zONgtjg==} - engines: {node: '>=6'} - dependencies: - time-zone: 1.0.0 - dev: true - /dateformat@4.6.3: resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==} dev: true @@ -16667,6 +16676,11 @@ packages: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} dev: true + /diff-sequences@29.6.3: + resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: true + /diff@3.5.0: resolution: {integrity: sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==} engines: {node: '>=0.3.1'} @@ -17318,8 +17332,8 @@ packages: peerDependencies: eslint: '>=8.0.0' dependencies: - acorn: 8.8.2 - acorn-jsx: 5.3.2(acorn@8.8.2) + acorn: 8.10.0 + acorn-jsx: 5.3.2(acorn@8.10.0) eslint: 8.43.0(patch_hash=fjbpfrtrjd6idngyeqxnwopfva) espree: 9.5.2 estree-util-visit: 1.2.1 @@ -17688,8 +17702,8 @@ packages: resolution: {integrity: sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: - acorn: 8.8.2 - acorn-jsx: 5.3.2(acorn@8.8.2) + acorn: 8.10.0 + acorn-jsx: 5.3.2(acorn@8.10.0) eslint-visitor-keys: 3.4.1 dev: true @@ -18045,10 +18059,6 @@ packages: /fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - /fast-diff@1.2.0: - resolution: {integrity: sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==} - dev: true - /fast-glob@3.2.12: resolution: {integrity: sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==} engines: {node: '>=8.6.0'} @@ -18058,6 +18068,17 @@ packages: glob-parent: 5.1.2 merge2: 1.4.1 micromatch: 4.0.5 + dev: true + + /fast-glob@3.3.1: + resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==} + engines: {node: '>=8.6.0'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.5 /fast-json-parse@1.0.3: resolution: {integrity: sha512-FRWsaZRWEJ1ESVNbDWmsAlqDk96gPQezzLghafp5J4GUKjbCz3OkAHuZs5TuPEtkbVQERysLp9xv6c24fBm8Aw==} @@ -18222,6 +18243,10 @@ packages: resolution: {integrity: sha512-LXcdgpdcVedccGg0AZqg+S8lX/FCdwXD92WNZ5k5qsb0irRhSFsBOpcJt7oevyqT2/C2nEE0zSFNdBEpj3YOSw==} dev: true + /fflate@0.8.0: + resolution: {integrity: sha512-FAdS4qMuFjsJj6XHbBaZeXOgaypXp8iw/Tpyuq/w3XA41jjLHT8NPA+n7czH/DDhdncq0nAyDZmPeWXh2qmdIg==} + dev: true + /figgy-pudding@3.5.2: resolution: {integrity: sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw==} dev: false @@ -18899,7 +18924,7 @@ packages: https-proxy-agent: 5.0.1 mri: 1.2.0 node-fetch-native: 1.0.2 - pathe: 1.1.0 + pathe: 1.1.1 tar: 6.1.13 transitivePeerDependencies: - supports-color @@ -19074,7 +19099,7 @@ packages: dependencies: array-union: 2.1.0 dir-glob: 3.0.1 - fast-glob: 3.2.12 + fast-glob: 3.3.1 ignore: 5.2.4 merge2: 1.4.1 slash: 3.0.0 @@ -19084,7 +19109,7 @@ packages: engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} dependencies: dir-glob: 3.0.1 - fast-glob: 3.2.12 + fast-glob: 3.3.1 ignore: 5.2.4 merge2: 1.4.1 slash: 4.0.0 @@ -21067,11 +21092,6 @@ packages: engines: {node: '>=0.10.0'} dev: true - /js-string-escape@1.0.1: - resolution: {integrity: sha512-Smw4xcfIQ5LVjAOuJCvN/zIodzA/BBSsluuoSykP+lUvScIi4U6RJLfwHet5cxFnCswUjISV8oAXaqaJDY3chg==} - engines: {node: '>= 0.8'} - dev: true - /js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -21218,7 +21238,7 @@ packages: resolution: {integrity: sha512-qCRJWlbP2v6HbmKW7R3lFbeiVWHo+oMJ0j+MizwvauqnCV/EvtAeEeuCgoc/ErtsuoKgYB8U4Ih8AxJbXoE6/g==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: - acorn: 8.8.2 + acorn: 8.10.0 eslint-visitor-keys: 3.4.1 espree: 9.5.2 semver: 7.5.4 @@ -21914,14 +21934,14 @@ packages: resolution: {integrity: sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==} engines: {node: '>=12'} dependencies: - '@jridgewell/sourcemap-codec': 1.4.14 + '@jridgewell/sourcemap-codec': 1.4.15 dev: false - /magic-string@0.30.0: - resolution: {integrity: sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==} + /magic-string@0.30.3: + resolution: {integrity: sha512-B7xGbll2fG/VjP+SWg4sX3JynwIU0mjoTc6MPpKNuIvftk6u6vqhDnk1R80b8C2GBR6ywqy+1DcKBrevBg+bmw==} engines: {node: '>=12'} dependencies: - '@jridgewell/sourcemap-codec': 1.4.14 + '@jridgewell/sourcemap-codec': 1.4.15 dev: true /mailcomposer@3.12.0: @@ -22090,13 +22110,6 @@ packages: remove-accents: 0.4.2 dev: false - /md5-hex@3.0.1: - resolution: {integrity: sha512-BUiRtTtV39LIJwinWBjqVsU9xhdnz7/i889V859IBFpuqGAj6LuOvHv5XLbgZ2R7ptJoJaEcxkv88/h25T7Ciw==} - engines: {node: '>=8'} - dependencies: - blueimp-md5: 2.19.0 - dev: true - /md5.js@1.3.5: resolution: {integrity: sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==} dependencies: @@ -22608,8 +22621,8 @@ packages: /micromark-extension-mdxjs@1.0.0: resolution: {integrity: sha512-TZZRZgeHvtgm+IhtgC2+uDMR7h8eTKF0QUX9YsgoL9+bADBpBY6SiLvWqnBlLbCEevITmTqmEuY3FoxMKVs1rQ==} dependencies: - acorn: 8.8.2 - acorn-jsx: 5.3.2(acorn@8.8.2) + acorn: 8.10.0 + acorn-jsx: 5.3.2(acorn@8.10.0) micromark-extension-mdx-expression: 1.0.3 micromark-extension-mdx-jsx: 1.0.3 micromark-extension-mdx-md: 1.0.0 @@ -23434,13 +23447,13 @@ packages: hasBin: true dev: false - /mlly@1.2.0: - resolution: {integrity: sha512-+c7A3CV0KGdKcylsI6khWyts/CYrGTrRVo4R/I7u/cUsy0Conxa6LUhiEzVKIw14lc2L5aiO4+SeVe4TeGRKww==} + /mlly@1.4.2: + resolution: {integrity: sha512-i/Ykufi2t1EZ6NaPLdfnZk2AX8cs0d+mTzVKuPfqPKPatxLApaBoxJQ9x1/uckXtrS/U5oisPMDkNs0yQTaBRg==} dependencies: - acorn: 8.8.2 - pathe: 1.1.0 - pkg-types: 1.0.2 - ufo: 1.1.1 + acorn: 8.10.0 + pathe: 1.1.1 + pkg-types: 1.0.3 + ufo: 1.3.0 dev: true /module-details-from-path@1.0.3: @@ -25010,8 +25023,8 @@ packages: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} - /pathe@1.1.0: - resolution: {integrity: sha512-ODbEPR0KKHqECXW1GoxdDb+AZvULmXjVPy4rt+pGo2+TnjJTIPJQSVS6N63n8T2Ip+syHhbn52OewKicV0373w==} + /pathe@1.1.1: + resolution: {integrity: sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q==} dev: true /pathval@1.1.1: @@ -25354,12 +25367,12 @@ packages: find-up: 5.0.0 dev: true - /pkg-types@1.0.2: - resolution: {integrity: sha512-hM58GKXOcj8WTqUXnsQyJYXdeAPbythQgEF3nTcEo+nkD49chjQ9IKm/QJy9xf6JakXptz86h7ecP2024rrLaQ==} + /pkg-types@1.0.3: + resolution: {integrity: sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==} dependencies: jsonc-parser: 3.2.0 - mlly: 1.2.0 - pathe: 1.1.0 + mlly: 1.4.2 + pathe: 1.1.1 dev: true /pluralize@8.0.0: @@ -25996,20 +26009,20 @@ packages: renderkid: 3.0.0 dev: true - /pretty-format@27.5.1: - resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + /pretty-format@29.4.1: + resolution: {integrity: sha512-dt/Z761JUVsrIKaY215o1xQJBGlSmTx/h4cSqXqjHLnU1+Kt+mavVE7UgqJJO5ukx5HjSswHfmXz4LjS2oIJfg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - ansi-regex: 5.0.1 + '@jest/schemas': 29.6.3 ansi-styles: 5.2.0 - react-is: 17.0.2 + react-is: 18.2.0 dev: true - /pretty-format@29.4.1: - resolution: {integrity: sha512-dt/Z761JUVsrIKaY215o1xQJBGlSmTx/h4cSqXqjHLnU1+Kt+mavVE7UgqJJO5ukx5HjSswHfmXz4LjS2oIJfg==} + /pretty-format@29.6.3: + resolution: {integrity: sha512-ZsBgjVhFAj5KeK+nHfF1305/By3lechHQSMWCTl8iHSbfOm2TN5nHEtFc/+W7fAyUeCs2n5iow72gld4gW0xDw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/schemas': 29.4.3 + '@jest/schemas': 29.6.3 ansi-styles: 5.2.0 react-is: 18.2.0 dev: true @@ -26485,10 +26498,6 @@ packages: /react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} - /react-is@17.0.2: - resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} - dev: true - /react-is@18.1.0: resolution: {integrity: sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg==} dev: true @@ -27819,6 +27828,15 @@ packages: mrmime: 1.0.1 totalist: 1.1.0 + /sirv@2.0.3: + resolution: {integrity: sha512-O9jm9BsID1P+0HOi81VpXPoDxYP374pkOLzACAoyUQ/3OUVndNpsz6wMnY2z+yOxzbllCKZrM+9QrWsv4THnyA==} + engines: {node: '>= 10'} + dependencies: + '@polka/url': 1.0.0-next.21 + mrmime: 1.0.1 + totalist: 3.0.1 + dev: true + /sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} dev: true @@ -28264,6 +28282,10 @@ packages: resolution: {integrity: sha512-uUZI65yrV2Qva5gqE0+A7uVAvO40iPo6jGhs7s8keRfHCmtg+uB2X6EiLGCI9IgL1J17xGhvoOqSz79lzICPTA==} dev: true + /std-env@3.4.3: + resolution: {integrity: sha512-f9aPhy8fYBuMN+sNfakZV18U39PbalgjXG3lLB9WkaYTxijru61wb57V9wxxNthXM5Sd88ETBWi29qLAsHO52Q==} + dev: true + /store2@2.14.2: resolution: {integrity: sha512-siT1RiqlfQnGqgT/YzXVUNsom9S0H1OX+dpdGN1xkyYATo4I6sep5NmsRD/40s3IIOvlCq6akxkqG82urIZW1w==} dev: true @@ -28524,7 +28546,7 @@ packages: /strip-literal@1.0.1: resolution: {integrity: sha512-QZTsipNpa2Ppr6v1AmJHESqJ3Uz247MUS0OjrnnZjFAvEoWqxuyFuXn2xLgMtRnijJShAa1HL0gtJyUs7u7n3Q==} dependencies: - acorn: 8.8.2 + acorn: 8.10.0 dev: true /stripe@12.9.0: @@ -28945,7 +28967,7 @@ packages: engines: {node: '>=6.0.0'} hasBin: true dependencies: - acorn: 8.8.2 + acorn: 8.10.0 commander: 2.20.3 source-map: 0.6.1 source-map-support: 0.5.21 @@ -28957,7 +28979,7 @@ packages: hasBin: true dependencies: '@jridgewell/source-map': 0.3.2 - acorn: 8.8.2 + acorn: 8.10.0 commander: 2.20.3 source-map-support: 0.5.21 @@ -29024,11 +29046,6 @@ packages: /through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} - /time-zone@1.0.0: - resolution: {integrity: sha512-TIsDdtKo6+XrPtiTm1ssmMngN1sAhyKnTO2kunQWqNPWIVvCm15Wmw4SWInwTVgJ5u/Tr04+8Ei9TNcw4x4ONA==} - engines: {node: '>=4'} - dev: true - /timers-browserify@2.0.12: resolution: {integrity: sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==} engines: {node: '>=0.6.0'} @@ -29058,13 +29075,13 @@ packages: resolution: {integrity: sha512-kRwSG8Zx4tjF9ZiyH4bhaebu+EDz1BOx9hOigYHlUW4xxI/wKIUQUqo018UlU4ar6ATPBsaMrdbKZ+tmPdohFA==} dev: true - /tinypool@0.5.0: - resolution: {integrity: sha512-paHQtnrlS1QZYKF/GnLoOM/DN9fqaGOFbCbxzAhwniySnzl9Ebk8w73/dd34DAhe/obUbPAOldTyYXQZxnPBPQ==} + /tinypool@0.7.0: + resolution: {integrity: sha512-zSYNUlYSMhJ6Zdou4cJwo/p7w5nmAH17GRfU/ui3ctvjXFErXXkruT4MWW6poDeXgCaIBlGLrfU6TbTXxyGMww==} engines: {node: '>=14.0.0'} dev: true - /tinyspy@2.1.0: - resolution: {integrity: sha512-7eORpyqImoOvkQJCSkL0d0mB4NHHIFAy4b1u8PHdDa7SjGS2njzl6/lyGoZLm+eyYEtlUmFGE0rFj66SWxZgQQ==} + /tinyspy@2.1.1: + resolution: {integrity: sha512-XPJL2uSzcOyBMky6OFrusqWlzfFrXtE0hPuMgW8A2HmaqrPo4ZQHRN/V0QXN3FSjKxpsbRrFc5LI7KOwBsT1/w==} engines: {node: '>=14.0.0'} dev: true @@ -29168,6 +29185,11 @@ packages: resolution: {integrity: sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g==} engines: {node: '>=6'} + /totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + dev: true + /toucan-js@3.2.3: resolution: {integrity: sha512-5sdXtcJfgFXfvn6R4GwsNriUQp6RVdIeKc/+xkyXZqGw56fY2oP4+TTPY8LtojgRMiDZhvD4nFxIIl/Ctple+Q==} dependencies: @@ -29618,8 +29640,8 @@ packages: resolution: {integrity: sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==} dev: false - /ufo@1.1.1: - resolution: {integrity: sha512-MvlCc4GHrmZdAllBc0iUDowff36Q9Ndw/UzqmEKyrfSzokTd9ZCy1i+IIk5hrYKkjoYVQyNbrw7/F8XJ2rEwTg==} + /ufo@1.3.0: + resolution: {integrity: sha512-bRn3CsoojyNStCZe0BG0Mt4Nr/4KF+rhFlnNXybgqt5pXHNFRlqinSoQaTrGyzE4X8aHplSb+TorH+COin9Yxw==} dev: true /uglify-js@3.17.3: @@ -29928,7 +29950,7 @@ packages: /unplugin@0.10.2: resolution: {integrity: sha512-6rk7GUa4ICYjae5PrAllvcDeuT8pA9+j5J5EkxbMFaV+SalHhxZ7X2dohMzu6C3XzsMT+6jwR/+pwPNR3uK9MA==} dependencies: - acorn: 8.8.2 + acorn: 8.10.0 chokidar: 3.5.3 webpack-sources: 3.2.3 webpack-virtual-modules: 0.4.6 @@ -30313,15 +30335,15 @@ packages: replace-ext: 1.0.1 dev: true - /vite-node@0.31.1(@types/node@18.16.19): - resolution: {integrity: sha512-BajE/IsNQ6JyizPzu9zRgHrBwczkAs0erQf/JRpgTIESpKvNj9/Gd0vxX905klLkb0I0SJVCKbdrl5c6FnqYKA==} + /vite-node@0.34.3(@types/node@18.16.19): + resolution: {integrity: sha512-+0TzJf1g0tYXj6tR2vEyiA42OPq68QkRZCu/ERSo2PtsDJfBpDyEfuKbRvLmZqi/CgC7SCBtyC+WjTGNMRIaig==} engines: {node: '>=v14.18.0'} hasBin: true dependencies: cac: 6.7.14 debug: 4.3.4(supports-color@8.1.1) - mlly: 1.2.0 - pathe: 1.1.0 + mlly: 1.4.2 + pathe: 1.1.1 picocolors: 1.0.0 vite: 4.1.1(@types/node@18.16.19) transitivePeerDependencies: @@ -30368,8 +30390,8 @@ packages: fsevents: 2.3.2 dev: true - /vitest@0.31.1: - resolution: {integrity: sha512-/dOoOgzoFk/5pTvg1E65WVaobknWREN15+HF+0ucudo3dDG/vCZoXTQrjIfEaWvQXmqScwkRodrTbM/ScMpRcQ==} + /vitest@0.34.3(@vitest/ui@0.34.3): + resolution: {integrity: sha512-7+VA5Iw4S3USYk+qwPxHl8plCMhA5rtfwMjgoQXMT7rO5ldWcdsdo3U1QD289JgglGK4WeOzgoLTsGFu6VISyQ==} engines: {node: '>=v14.18.0'} hasBin: true peerDependencies: @@ -30402,27 +30424,27 @@ packages: '@types/chai': 4.3.5 '@types/chai-subset': 1.3.3 '@types/node': 18.16.19 - '@vitest/expect': 0.31.1 - '@vitest/runner': 0.31.1 - '@vitest/snapshot': 0.31.1 - '@vitest/spy': 0.31.1 - '@vitest/utils': 0.31.1 - acorn: 8.8.2 + '@vitest/expect': 0.34.3 + '@vitest/runner': 0.34.3 + '@vitest/snapshot': 0.34.3 + '@vitest/spy': 0.34.3 + '@vitest/ui': 0.34.3(vitest@0.34.3) + '@vitest/utils': 0.34.3 + acorn: 8.10.0 acorn-walk: 8.2.0 cac: 6.7.14 chai: 4.3.7 - concordance: 5.0.4 debug: 4.3.4(supports-color@8.1.1) local-pkg: 0.4.3 - magic-string: 0.30.0 - pathe: 1.1.0 + magic-string: 0.30.3 + pathe: 1.1.1 picocolors: 1.0.0 - std-env: 3.3.2 + std-env: 3.4.3 strip-literal: 1.0.1 tinybench: 2.5.0 - tinypool: 0.5.0 + tinypool: 0.7.0 vite: 4.1.1(@types/node@18.16.19) - vite-node: 0.31.1(@types/node@18.16.19) + vite-node: 0.34.3(@types/node@18.16.19) why-is-node-running: 2.2.2 transitivePeerDependencies: - less @@ -30547,7 +30569,7 @@ packages: engines: {node: '>= 10.13.0'} hasBin: true dependencies: - acorn: 8.8.2 + acorn: 8.10.0 acorn-walk: 8.2.0 chalk: 4.1.2 commander: 7.2.0 @@ -30652,8 +30674,8 @@ packages: '@webassemblyjs/ast': 1.11.1 '@webassemblyjs/wasm-edit': 1.11.1 '@webassemblyjs/wasm-parser': 1.11.1 - acorn: 8.8.2 - acorn-import-assertions: 1.8.0(acorn@8.8.2) + acorn: 8.10.0 + acorn-import-assertions: 1.8.0(acorn@8.10.0) browserslist: 4.21.5 chrome-trace-event: 1.0.3 enhanced-resolve: 5.12.0 @@ -30676,11 +30698,6 @@ packages: - esbuild - uglify-js - /well-known-symbols@2.0.0: - resolution: {integrity: sha512-ZMjC3ho+KXo0BfJb7JgtQ5IBuvnShdlACNkKkdsqBmYw3bPAaJfPeYUo6tLUaT5tG/Gkh7xkpBhKRQ9e7pyg9Q==} - engines: {node: '>=6'} - dev: true - /whatwg-mimetype@3.0.0: resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} engines: {node: '>=12'} diff --git a/scripts/serializer.ts b/scripts/serializer.ts index 951d8ce8dfa..3cb1cc488dc 100644 --- a/scripts/serializer.ts +++ b/scripts/serializer.ts @@ -1,28 +1,9 @@ import rawSnapshotSerializer from 'jest-snapshot-serializer-raw/always'; import { expect } from 'vitest'; +import { normalizeCliOutput } from './serializers/cli-output'; expect.addSnapshotSerializer(rawSnapshotSerializer); -function normalizeCliOutput(value: string) { - return value - .split('\n') - .map(line => - line - .replaceAll('✔', 'v') - .replaceAll('ℹ', 'i') - // eslint-disable-next-line no-control-regex - .replace(/\x1B[[(?);]{0,2}(;?\d)*./g, '') - .replace( - /http:\/\/localhost:8080\/[$]*\w+\/[$]*\w+\/production/i, - 'http://localhost:8080/$organization/$project/production', - ) - .replace(/history\/[$]*\w+-\w+-\w+-\w+-\w+/i, 'history/$version') - .trim(), - ) - .filter(Boolean) - .join('\n'); -} - expect.addSnapshotSerializer({ test: value => typeof value === 'string' && (value.includes('✔') || value.includes('ℹ')), print: value => normalizeCliOutput(value as string), diff --git a/scripts/serializers/cli-output.ts b/scripts/serializers/cli-output.ts new file mode 100644 index 00000000000..2fc9f7c9ac4 --- /dev/null +++ b/scripts/serializers/cli-output.ts @@ -0,0 +1,19 @@ +export function normalizeCliOutput(value: string) { + return value + .split('\n') + .map(line => + line + .replaceAll('✔', 'v') + .replaceAll('ℹ', 'i') + // eslint-disable-next-line no-control-regex + .replace(/\x1B[[(?);]{0,2}(;?\d)*./g, '') + .replace( + /http:\/\/localhost:8080\/[$]*\w+\/[$]*\w+\/production/i, + 'http://localhost:8080/$organization/$project/production', + ) + .replace(/history\/[$]*\w+-\w+-\w+-\w+-\w+/i, 'history/$version') + .trim(), + ) + .filter(Boolean) + .join('\n'); +}