Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
378 changes: 189 additions & 189 deletions composition-go/index.global.js

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion composition/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
"test:watch": "vitest test",
"test": "vitest run",
"lint": "prettier --check src tests",
"lint:fix": "prettier --write src tests"
"lint:fix": "prettier --write src tests",
"postversion": "node ./scripts/get-composition-version.mjs"
},
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
Expand Down
20 changes: 20 additions & 0 deletions composition/scripts/get-composition-version.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import fs from 'fs';
import { fileURLToPath } from 'node:url';
import path from 'node:path';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const compositionVersion = '{{$COMPOSITION__VERSION}}';

// From pnpm v10+, modules will explicitly need to set whether a hook is allowed to run.
if (process.argv[1] === __filename) {
const json = JSON.parse(fs.readFileSync(path.join(__dirname, '../package.json')).toString());
const version = json.version;
const varFilePath = path.join(__dirname, '../dist/utils/composition-version.js');
let content = fs.readFileSync(varFilePath).toString();
if (content.indexOf(compositionVersion) < 0) {
throw new Error(`"${compositionVersion}" string not found in dist/utils/composition-version.js.`);
}
content = content.replace(compositionVersion, version);
fs.writeFileSync(varFilePath, content);
}
3 changes: 2 additions & 1 deletion composition/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ export * from './schema-building/type-definition-data';
export * from './schema-building/type-merging';
export * from './schema-building/utils';
export * from './subgraph/subgraph';
export * from './utils/utils';
export * from './utils/composition-version';
export * from './utils/constants';
export * from './utils/utils';
export * from './utils/string-constants';
export * from './warnings/warnings';
1 change: 1 addition & 0 deletions composition/src/utils/composition-version.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const COMPOSITION_VERSION = '{{$COMPOSITION__VERSION}}';
3 changes: 2 additions & 1 deletion composition/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
"compilerOptions": {
"declaration": true,
"outDir": "./dist",
"module": "commonjs",
"module": "NodeNext",
"moduleResolution": "NodeNext",
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
Expand Down
1,151 changes: 581 additions & 570 deletions connect-go/gen/proto/wg/cosmo/node/v1/node.pb.go

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions connect/src/wg/cosmo/node/v1/node_pb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,11 @@ export class RouterConfig extends Message<RouterConfig> {
*/
featureFlagConfigs?: FeatureFlagRouterExecutionConfigs;

/**
* @generated from field: string compatibility_version = 5;
*/
compatibilityVersion = "";

constructor(data?: PartialMessage<RouterConfig>) {
super();
proto3.util.initPartial(data, this);
Expand All @@ -342,6 +347,7 @@ export class RouterConfig extends Message<RouterConfig> {
{ no: 2, name: "version", kind: "scalar", T: 9 /* ScalarType.STRING */ },
{ no: 3, name: "subgraphs", kind: "message", T: Subgraph, repeated: true },
{ no: 4, name: "feature_flag_configs", kind: "message", T: FeatureFlagRouterExecutionConfigs, opt: true },
{ no: 5, name: "compatibility_version", kind: "scalar", T: 9 /* ScalarType.STRING */ },
]);

static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): RouterConfig {
Expand Down
1 change: 1 addition & 0 deletions proto/wg/cosmo/node/v1/node.proto
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ message RouterConfig {
repeated Subgraph subgraphs = 3;
// The map of feature flag router execution configs requires a wrapper to be non-breaking
optional FeatureFlagRouterExecutionConfigs feature_flag_configs = 4;
string compatibility_version = 5;
}

message Response {
Expand Down
10 changes: 10 additions & 0 deletions router/core/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -1105,6 +1105,16 @@ func (r *Router) Start(ctx context.Context) error {
return nil
}

/* Older versions of composition will not populate a compatibility version.
* Currently, all "old" router execution configurations are compatible as there have been no breaking
* changes.
* Upon the first breaking change to the execution config, an unpopulated compatibility version will
* also be unsupported (and the logic for IsRouterCompatibleWithExecutionConfig will need to be updated).
*/
if !execution_config.IsRouterCompatibleWithExecutionConfig(r.logger, cfg.CompatibilityVersion) {
return nil
}

if err := r.newServer(ctx, cfg); err != nil {
r.logger.Error("Failed to update server with new config", zap.Error(err))
return nil
Expand Down
1,151 changes: 581 additions & 570 deletions router/gen/proto/wg/cosmo/node/v1/node.pb.go

Large diffs are not rendered by default.

72 changes: 72 additions & 0 deletions router/pkg/execution_config/compatibility.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package execution_config

import (
"fmt"
"go.uber.org/zap"
"strconv"
"strings"
)

const (
// ExecutionConfigVersionThreshold should ONLY be updated if there is a breaking change in the router execution config.
ExecutionConfigVersionThreshold = 1
compatibilityVersionParseErrorMessage = "Failed to parse compatibility version."
executionConfigVersionParseErrorMessage = "Failed to parse router execution config version of compatibility version."
)

func IsRouterCompatibleWithExecutionConfig(logger *zap.Logger, compatibilityVersion string) bool {
if compatibilityVersion == "" {
return true
}
/* A compatibility version is composed thus: <router execution configuration version>:<composition package version>
* A router version supports a maximum router execution configuration version (ExecutionConfigVersionThreshold).
* In the event the execution config version exceeds ExecutionConfigVersionThreshold, an error will request for
* the router version be upgraded.
* If the router version requires a newer router execution configuration version, a warning will explain that some
* new features may be unavailable or functionality/behaviour may have changed.
*/
segments := strings.Split(compatibilityVersion, ":")
if len(segments) != 2 {
logger.Error(compatibilityVersionParseErrorMessage, zap.String("compatibility_version", compatibilityVersion))
return false
}
routerExecutionVersion, err := strconv.ParseInt(segments[0], 10, 32)
if err != nil {
logger.Error(executionConfigVersionParseErrorMessage, zap.String("compatibility_version", compatibilityVersion))
return false
}
switch {
case routerExecutionVersion == ExecutionConfigVersionThreshold:
return true
case routerExecutionVersion > ExecutionConfigVersionThreshold:
logger.Error(
executionConfigVersionThresholdExceededError(routerExecutionVersion),
zap.Int64("execution_config_version", routerExecutionVersion),
zap.String("composition_package_version", segments[1]),
)
return false
default:
logger.Warn(
executionConfigVersionInsufficientWarning(routerExecutionVersion),
zap.Int64("execution_config_version", routerExecutionVersion),
zap.String("composition_package_version", segments[1]),
)
return true
}
}

func executionConfigVersionThresholdExceededError(executionConfigVersion int64) string {
return fmt.Sprintf(
"This router version supports a router execution config version up to %d. The router execution config version supplied is %d. Please upgrade your router version.",
ExecutionConfigVersionThreshold,
executionConfigVersion,
)
}

func executionConfigVersionInsufficientWarning(executionConfigVersion int64) string {
return fmt.Sprintf(
"This router version requires a minimum router execution config version of %d to support all functionality. The router execution config version supplied is %d. Please create a new execution configuration.",
ExecutionConfigVersionThreshold,
executionConfigVersion,
)
}
108 changes: 108 additions & 0 deletions router/pkg/execution_config/compatibility_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package execution_config

import (
"bytes"
"fmt"
"github.com/stretchr/testify/assert"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"go.uber.org/zap/zaptest/observer"
"testing"
)

func TestExecutionConfiguration(t *testing.T) {
t.Run("no compatibility version is supported", func(t *testing.T) {
observed, logs := observer.New(zapcore.DebugLevel)
logger := newLogger(observed)
assert.True(t, IsRouterCompatibleWithExecutionConfig(logger, ""))
assert.Equal(t, 0, len(logs.All()))
})

t.Run("same compatibility version is supported", func(t *testing.T) {
observed, logs := observer.New(zapcore.DebugLevel)
logger := newLogger(observed)
assert.True(t, IsRouterCompatibleWithExecutionConfig(logger, fmt.Sprintf("%d:0.1.0", ExecutionConfigVersionThreshold)))
assert.Equal(t, 0, len(logs.All()))
})

t.Run("return an error if compatibility version is unparsable #1", func(t *testing.T) {
observed, logs := observer.New(zapcore.DebugLevel)
logger := newLogger(observed)
compatibilityVersion := "nonsense"
assert.False(t, IsRouterCompatibleWithExecutionConfig(logger, compatibilityVersion))
logsSlice := logs.All()
assert.Equal(t, 1, len(logsSlice))
assert.Equal(t, compatibilityVersionParseErrorMessage, logsSlice[0].Message)
assert.Equal(t, zapcore.ErrorLevel, logsSlice[0].Level)
assert.Equal(t, 1, len(logsSlice[0].Context))
assert.Equal(t, zap.String("compatibility_version", compatibilityVersion), logsSlice[0].Context[0])
})

t.Run("return an error if compatibility version is unparsable #2", func(t *testing.T) {
observed, logs := observer.New(zapcore.DebugLevel)
logger := newLogger(observed)
compatibilityVersion := "1:2:3"
assert.False(t, IsRouterCompatibleWithExecutionConfig(logger, compatibilityVersion))
logsSlice := logs.All()
assert.Equal(t, 1, len(logsSlice))
assert.Equal(t, compatibilityVersionParseErrorMessage, logsSlice[0].Message)
assert.Equal(t, zapcore.ErrorLevel, logsSlice[0].Level)
assert.Equal(t, 1, len(logsSlice[0].Context))
assert.Equal(t, zap.String("compatibility_version", compatibilityVersion), logsSlice[0].Context[0])
})

t.Run("return an error if execution config version is unparsable", func(t *testing.T) {
observed, logs := observer.New(zapcore.DebugLevel)
logger := newLogger(observed)
compatibilityVersion := "a:0.1.0"
assert.False(t, IsRouterCompatibleWithExecutionConfig(logger, compatibilityVersion))
logsSlice := logs.All()
assert.Equal(t, 1, len(logsSlice))
assert.Equal(t, executionConfigVersionParseErrorMessage, logsSlice[0].Message)
assert.Equal(t, zapcore.ErrorLevel, logsSlice[0].Level)
assert.Equal(t, 1, len(logsSlice[0].Context))
assert.Equal(t, zap.String("compatibility_version", compatibilityVersion), logsSlice[0].Context[0])
})

t.Run("return an error if the maximum execution config version threshold of the router is exceeded", func(t *testing.T) {
observed, logs := observer.New(zapcore.DebugLevel)
logger := newLogger(observed)
nextVersion := int64(ExecutionConfigVersionThreshold + 1)
compVersion := "0.1.0"
compatibilityVersion := fmt.Sprintf("%d:%s", nextVersion, compVersion)
assert.False(t, IsRouterCompatibleWithExecutionConfig(logger, compatibilityVersion))
logsSlice := logs.All()
assert.Equal(t, 1, len(logsSlice))
assert.Equal(t, executionConfigVersionThresholdExceededError(nextVersion), logsSlice[0].Message)
assert.Equal(t, zapcore.ErrorLevel, logsSlice[0].Level)
assert.Equal(t, 2, len(logsSlice[0].Context))
assert.Equal(t, zap.Int64("execution_config_version", nextVersion), logsSlice[0].Context[0])
assert.Equal(t, zap.String("composition_package_version", compVersion), logsSlice[0].Context[1])
})

t.Run("return a warning if the execution config version is insufficient", func(t *testing.T) {
observed, logs := observer.New(zapcore.DebugLevel)
logger := newLogger(observed)
previousVersion := int64(ExecutionConfigVersionThreshold - 1)
compVersion := "0.1.0"
compatibilityVersion := fmt.Sprintf("%d:%s", previousVersion, compVersion)
assert.True(t, IsRouterCompatibleWithExecutionConfig(logger, compatibilityVersion))
logsSlice := logs.All()
assert.Equal(t, 1, len(logsSlice))
assert.Equal(t, executionConfigVersionInsufficientWarning(previousVersion), logsSlice[0].Message)
assert.Equal(t, zapcore.WarnLevel, logsSlice[0].Level)
assert.Equal(t, 2, len(logsSlice[0].Context))
assert.Equal(t, zap.Int64("execution_config_version", previousVersion), logsSlice[0].Context[0])
assert.Equal(t, zap.String("composition_package_version", compVersion), logsSlice[0].Context[1])
})
}

func newLogger(observed zapcore.Core) *zap.Logger {
var buffer bytes.Buffer
return zap.New(
zapcore.NewTee(
zapcore.NewCore(zapcore.NewJSONEncoder(zapcore.EncoderConfig{}), zapcore.AddSync(&buffer), zapcore.DebugLevel),
observed,
),
)
}
3 changes: 2 additions & 1 deletion shared/src/router-config/builder.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import crypto from 'node:crypto';
import { printSchemaWithDirectives } from '@graphql-tools/utils';
import { ConfigurationData, FieldConfiguration, ROOT_TYPE_NAMES } from '@wundergraph/composition';
import { COMPOSITION_VERSION, ConfigurationData, FieldConfiguration, ROOT_TYPE_NAMES } from '@wundergraph/composition';
import { GraphQLSchema, lexicographicSortSchema } from 'graphql';
import {
GraphQLSubscriptionProtocol,
Expand Down Expand Up @@ -215,5 +215,6 @@ export const buildRouterConfig = function (input: Input): RouterConfig {
name: s.name,
routingUrl: s.url,
})),
compatibilityVersion: `1:${COMPOSITION_VERSION}`,
});
};
12 changes: 8 additions & 4 deletions shared/test/__snapshots__/router.config.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,8 @@ exports[`Router Config Builder > Build Subgraph schema > router.config.json 1`]
"name": "inventory",
"routingUrl": "https://wg-federation-demo-inventory.fly.dev/graphql"
}
]
],
"compatibilityVersion": "1:{{$COMPOSITION__VERSION}}"
}"
`;

Expand Down Expand Up @@ -436,7 +437,8 @@ exports[`Router Config Builder > that the federatedClientSDL property is not pro
"name": "products",
"routingUrl": "https://wg-federation-demo-products.fly.dev/graphql"
}
]
],
"compatibilityVersion": "1:{{$COMPOSITION__VERSION}}"
}"
`;

Expand Down Expand Up @@ -571,7 +573,8 @@ exports[`Router Config Builder > that the federatedClientSDL property is propaga
"name": "products",
"routingUrl": "https://wg-federation-demo-products.fly.dev/graphql"
}
]
],
"compatibilityVersion": "1:{{$COMPOSITION__VERSION}}"
}"
`;

Expand Down Expand Up @@ -706,6 +709,7 @@ exports[`Router Config Builder > that the federatedClientSDL property is propaga
"name": "products",
"routingUrl": "https://wg-federation-demo-products.fly.dev/graphql"
}
]
],
"compatibilityVersion": "1:{{$COMPOSITION__VERSION}}"
}"
`;