Skip to content

Commit 7b5b836

Browse files
authored
Avoid >= comparison for FeatureVersion objects (#2883)
This won't become a problem in practice until we have `major` or `minor` version numbers greater than 9, because `toString`-based comparison of single digits works as expected.
1 parent 160299f commit 7b5b836

File tree

5 files changed

+74
-8
lines changed

5 files changed

+74
-8
lines changed

.changeset/beige-pumas-lick.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@apollo/federation-internals": patch
3+
---
4+
5+
Avoid `>=` comparison for `FeatureVersion` objects

internals-js/src/specs/__tests__/coreSpec.test.ts

+45
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,51 @@ class TestFeatureDefinition extends FeatureDefinition {
217217
}
218218
}
219219

220+
describe('FeatureVersion', () => {
221+
it('toString-based comparisons', () => {
222+
const v2_3 = new FeatureVersion(2, 3);
223+
const v10_0 = new FeatureVersion(10, 0);
224+
225+
expect(v2_3.toString()).toBe('v2.3');
226+
expect(v10_0.toString()).toBe('v10.0');
227+
228+
// Operators like <, <=, >, and >= use lexicographic comparison on
229+
// version.toString() strings, but do not perform numeric lexicographic
230+
// comparison of the major and minor numbers, so 'v10...' < 'v2...' and the
231+
// following comparisons produce unintuitive results.
232+
expect([
233+
v2_3 < v10_0,
234+
v2_3 <= v10_0,
235+
v2_3 > v10_0,
236+
v2_3 >= v10_0,
237+
]).toEqual(
238+
// This should really be [true, true, false, false], if JavaScript
239+
// supported more flexible/general operator overloading.
240+
[false, false, true, true],
241+
);
242+
243+
expect(v2_3.compareTo(v10_0)).toBe(-1);
244+
expect(v10_0.compareTo(v2_3)).toBe(1);
245+
246+
expect(v2_3.strictlyGreaterThan(v10_0)).toBe(false);
247+
expect(v10_0.strictlyGreaterThan(v2_3)).toBe(true);
248+
249+
expect(v2_3.lt(v10_0)).toBe(true);
250+
expect(v2_3.lte(v10_0)).toBe(true);
251+
expect(v2_3.gt(v10_0)).toBe(false);
252+
expect(v2_3.gte(v10_0)).toBe(false);
253+
expect(v10_0.lt(v2_3)).toBe(false);
254+
expect(v10_0.lte(v2_3)).toBe(false);
255+
expect(v10_0.gt(v2_3)).toBe(true);
256+
expect(v10_0.gte(v2_3)).toBe(true);
257+
258+
expect(v2_3.equals(v10_0)).toBe(false);
259+
expect(v10_0.equals(v2_3)).toBe(false);
260+
expect(v2_3.equals(v2_3)).toBe(true);
261+
expect(v10_0.equals(v10_0)).toBe(true);
262+
});
263+
});
264+
220265
describe('getMinimumRequiredVersion tests', () => {
221266
it('various combinations', () => {
222267
const versions = new FeatureDefinitions<TestFeatureDefinition>('test')

internals-js/src/specs/coreSpec.ts

+16
Original file line numberDiff line numberDiff line change
@@ -690,6 +690,22 @@ export class FeatureVersion {
690690
return 0;
691691
}
692692

693+
public lt(other: FeatureVersion): boolean {
694+
return this.compareTo(other) < 0;
695+
}
696+
697+
public lte(other: FeatureVersion): boolean {
698+
return this.compareTo(other) <= 0;
699+
}
700+
701+
public gt(other: FeatureVersion): boolean {
702+
return this.compareTo(other) > 0;
703+
}
704+
705+
public gte(other: FeatureVersion): boolean {
706+
return this.compareTo(other) >= 0;
707+
}
708+
693709
/**
694710
* Return true if this FeatureVersion is strictly greater than the provided one,
695711
* where ordering is meant by major and then minor number.

internals-js/src/specs/federationSpec.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ export class FederationSpecDefinition extends FeatureDefinition {
119119
this.registerDirective(createDirectiveSpecification({
120120
name: FederationDirectiveName.SHAREABLE,
121121
locations: [DirectiveLocation.OBJECT, DirectiveLocation.FIELD_DEFINITION],
122-
repeatable: version >= (new FeatureVersion(2, 2)),
122+
repeatable: version.gte(new FeatureVersion(2, 2)),
123123
}));
124124

125125
this.registerSubFeature(INACCESSIBLE_VERSIONS.getMinimumRequiredVersion(version));
@@ -130,7 +130,7 @@ export class FederationSpecDefinition extends FeatureDefinition {
130130
args: [{ name: 'from', type: (schema) => new NonNullType(schema.stringType()) }],
131131
}));
132132

133-
if (version >= (new FeatureVersion(2, 1))) {
133+
if (version.gte(new FeatureVersion(2, 1))) {
134134
this.registerDirective(createDirectiveSpecification({
135135
name: FederationDirectiveName.COMPOSE_DIRECTIVE,
136136
locations: [DirectiveLocation.SCHEMA],
@@ -139,20 +139,20 @@ export class FederationSpecDefinition extends FeatureDefinition {
139139
}));
140140
}
141141

142-
if (version >= (new FeatureVersion(2, 3))) {
142+
if (version.gte(new FeatureVersion(2, 3))) {
143143
this.registerDirective(createDirectiveSpecification({
144144
name: FederationDirectiveName.INTERFACE_OBJECT,
145145
locations: [DirectiveLocation.OBJECT],
146146
}));
147147
this.registerSubFeature(TAG_VERSIONS.find(new FeatureVersion(0, 3))!);
148148
}
149149

150-
if (version >= (new FeatureVersion(2, 5))) {
150+
if (version.gte(new FeatureVersion(2, 5))) {
151151
this.registerSubFeature(AUTHENTICATED_VERSIONS.find(new FeatureVersion(0, 1))!);
152152
this.registerSubFeature(REQUIRES_SCOPES_VERSIONS.find(new FeatureVersion(0, 1))!);
153153
}
154154

155-
if (version >= (new FeatureVersion(2, 6))) {
155+
if (version.gte(new FeatureVersion(2, 6))) {
156156
this.registerSubFeature(POLICY_VERSIONS.find(new FeatureVersion(0, 1))!);
157157
}
158158
}

internals-js/src/specs/joinSpec.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ export class JoinSpecDefinition extends FeatureDefinition {
8383
joinType.addArgument('extension', new NonNullType(schema.booleanType()), false);
8484
joinType.addArgument('resolvable', new NonNullType(schema.booleanType()), true);
8585

86-
if (this.version >= (new FeatureVersion(0, 3))) {
86+
if (this.version.gte(new FeatureVersion(0, 3))) {
8787
joinType.addArgument('isInterfaceObject', new NonNullType(schema.booleanType()), false);
8888
}
8989
}
@@ -93,7 +93,7 @@ export class JoinSpecDefinition extends FeatureDefinition {
9393
// The `graph` argument used to be non-nullable, but @interfaceObject makes us add some field in
9494
// the supergraph that don't "directly" come from any subgraph (they indirectly are inherited from
9595
// an `@interfaceObject` type), and to indicate that, we use a `@join__field(graph: null)` annotation.
96-
const graphArgType = this.version >= (new FeatureVersion(0, 3))
96+
const graphArgType = this.version.gte(new FeatureVersion(0, 3))
9797
? graphEnum
9898
: new NonNullType(graphEnum);
9999
joinField.addArgument('graph', graphArgType);
@@ -115,7 +115,7 @@ export class JoinSpecDefinition extends FeatureDefinition {
115115
joinImplements.addArgument('interface', new NonNullType(schema.stringType()));
116116
}
117117

118-
if (this.version >= (new FeatureVersion(0, 3))) {
118+
if (this.version.gte(new FeatureVersion(0, 3))) {
119119
const joinUnionMember = this.addDirective(schema, 'unionMember').addLocations(DirectiveLocation.UNION);
120120
joinUnionMember.repeatable = true;
121121
joinUnionMember.addArgument('graph', new NonNullType(graphEnum));

0 commit comments

Comments
 (0)