Skip to content
Draft
Show file tree
Hide file tree
Changes from 3 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
68 changes: 34 additions & 34 deletions composition-go/index.global.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions composition/src/router-configuration/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export type EventConfiguration = KafkaEventConfiguration | NatsEventConfiguratio
export type SubscriptionFilterValue = boolean | null | number | string;

export type SubscriptionFieldCondition = {
bypassIfValuesNull?: boolean;
fieldPath: string[];
values: SubscriptionFilterValue[];
};
Expand Down
1 change: 1 addition & 0 deletions composition/src/utils/string-constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const AUTHENTICATED = 'authenticated';
export const ARGUMENT_DEFINITION_UPPER = 'ARGUMENT_DEFINITION';
export const BOOLEAN = 'boolean';
export const BOOLEAN_SCALAR = 'Boolean';
export const BYPASS_IF_VALUES_NULL = 'bypassIfValuesNull';
export const CHANNEL = 'channel';
export const CHANNELS = 'channels';
export const COMPOSE_DIRECTIVE = 'composeDirective';
Expand Down
8 changes: 8 additions & 0 deletions composition/src/v1/constants/non-directive-definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
import { stringToNamedTypeNode, stringToNameNode } from '../../ast/utils';
import {
AND_UPPER,
BOOLEAN_SCALAR,
BYPASS_IF_VALUES_NULL,
CONSUMER_INACTIVE_THRESHOLD,
CONSUMER_NAME,
EDFS_NATS_STREAM_CONFIGURATION,
Expand Down Expand Up @@ -114,12 +116,18 @@ export const SCOPE_SCALAR_DEFINITION: ScalarTypeDefinitionNode = {
};

/* input openfed__SubscriptionFieldCondition {
* bypassIfValuesNull: Boolean
* fieldPath: String!
* values: [openfed__SubscriptionFilterValue]!
* }
*/
export const SUBSCRIPTION_FIELD_CONDITION_DEFINITION: InputObjectTypeDefinitionNode = {
fields: [
{
kind: Kind.INPUT_VALUE_DEFINITION,
name: stringToNameNode(BYPASS_IF_VALUES_NULL),
type: stringToNamedTypeNode(BOOLEAN_SCALAR),
},
{
kind: Kind.INPUT_VALUE_DEFINITION,
name: stringToNameNode(FIELD_PATH),
Expand Down
24 changes: 24 additions & 0 deletions composition/src/v1/federation/federation-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,8 @@ import {
import {
AND_UPPER,
AUTHORIZATION_DIRECTIVES,
BOOLEAN,
BYPASS_IF_VALUES_NULL,
CONDITION,
DEPRECATED,
ENUM_VALUE,
Expand Down Expand Up @@ -2628,10 +2630,32 @@ export class FederationFactory {
const duplicatedFieldNames = new Set<string>();
const invalidFieldNames = new Set<string>();
const fieldErrorMessages: string[] = [];
let hasSeenBypassIfValuesNull = false;
for (const objectFieldNode of objectValueNode.fields) {
const inputFieldName = objectFieldNode.name.value;
const inputFieldPath = inputPath + `.${inputFieldName}`;
switch (inputFieldName) {
case BYPASS_IF_VALUES_NULL: {
if (hasSeenBypassIfValuesNull) {
hasErrors = true;
duplicatedFieldNames.add(BYPASS_IF_VALUES_NULL);
break;
}
hasSeenBypassIfValuesNull = true;
if (objectFieldNode.value.kind !== Kind.BOOLEAN) {
fieldErrorMessages.push(
invalidInputFieldTypeErrorMessage(inputFieldPath, BOOLEAN, kindToNodeType(objectFieldNode.value.kind)),
);
hasErrors = true;
break;
}
// Only persist when explicitly true. Explicit false collapses to undefined so the
// serializer can emit the smallest proto for unchanged configs.
if (objectFieldNode.value.value === true) {
condition.bypassIfValuesNull = true;
}
break;
Comment on lines +2633 to +2657
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Still fail IN conditions when fieldPath or values is missing.

This new optional field makes IN: { bypassIfValuesNull: true } validate successfully, because leftover entries in validFieldNames never flip hasErrors. That lets composition emit an empty fieldPath/values filter downstream.

Suggested fix
     }
-    if (!hasErrors) {
+    if (validFieldNames.size > 0) {
+      hasErrors = true;
+    }
+    if (!hasErrors) {
       return true;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let hasSeenBypassIfValuesNull = false;
for (const objectFieldNode of objectValueNode.fields) {
const inputFieldName = objectFieldNode.name.value;
const inputFieldPath = inputPath + `.${inputFieldName}`;
switch (inputFieldName) {
case BYPASS_IF_VALUES_NULL: {
if (hasSeenBypassIfValuesNull) {
hasErrors = true;
duplicatedFieldNames.add(BYPASS_IF_VALUES_NULL);
break;
}
hasSeenBypassIfValuesNull = true;
if (objectFieldNode.value.kind !== Kind.BOOLEAN) {
fieldErrorMessages.push(
invalidInputFieldTypeErrorMessage(inputFieldPath, BOOLEAN, kindToNodeType(objectFieldNode.value.kind)),
);
hasErrors = true;
break;
}
// Only persist when explicitly true. Explicit false collapses to undefined so the
// serializer can emit the smallest proto for unchanged configs.
if (objectFieldNode.value.value === true) {
condition.bypassIfValuesNull = true;
}
break;
let hasSeenBypassIfValuesNull = false;
for (const objectFieldNode of objectValueNode.fields) {
const inputFieldName = objectFieldNode.name.value;
const inputFieldPath = inputPath + `.${inputFieldName}`;
switch (inputFieldName) {
case BYPASS_IF_VALUES_NULL: {
if (hasSeenBypassIfValuesNull) {
hasErrors = true;
duplicatedFieldNames.add(BYPASS_IF_VALUES_NULL);
break;
}
hasSeenBypassIfValuesNull = true;
if (objectFieldNode.value.kind !== Kind.BOOLEAN) {
fieldErrorMessages.push(
invalidInputFieldTypeErrorMessage(inputFieldPath, BOOLEAN, kindToNodeType(objectFieldNode.value.kind)),
);
hasErrors = true;
break;
}
// Only persist when explicitly true. Explicit false collapses to undefined so the
// serializer can emit the smallest proto for unchanged configs.
if (objectFieldNode.value.value === true) {
condition.bypassIfValuesNull = true;
}
break;
}
if (validFieldNames.size > 0) {
hasErrors = true;
}
if (!hasErrors) {
return true;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@composition/src/v1/federation/federation-factory.ts` around lines 2535 -
2559, The parser currently lets an IN condition pass validation when only
BYPASS_IF_VALUES_NULL is present because validFieldNames leftovers don't flip
hasErrors; after parsing objectValueNode.fields for the IN condition (the switch
handling BYPASS_IF_VALUES_NULL and other keys), explicitly validate that
required keys "fieldPath" and "values" are present (check validFieldNames or
track presence of fieldPath/values), and if either is missing set hasErrors =
true and push appropriate field error messages (similar to
invalidInputFieldTypeErrorMessage) so you cannot emit an IN filter with empty
fieldPath/values even when condition.bypassIfValuesNull is true; ensure
duplicatedFieldNames/hasSeen handling remains unchanged for
BYPASS_IF_VALUES_NULL and reference the BYPASS_IF_VALUES_NULL constant,
condition.bypassIfValuesNull, validFieldNames, hasErrors, and objectValueNode to
locate where to add this check.

}
case FIELD_PATH: {
if (validFieldNames.has(FIELD_PATH)) {
validFieldNames.delete(FIELD_PATH);
Expand Down
204 changes: 200 additions & 4 deletions composition/tests/v1/directives/subscription-filter.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { describe, expect, test } from 'vitest';
import {
BOOLEAN,
CONDITION,
FIRST_ORDINAL,
inaccessibleSubscriptionFieldConditionFieldPathFieldErrorMessage,
Expand All @@ -19,6 +20,7 @@ import {
OBJECT,
parse,
ROUTER_COMPATIBILITY_VERSION_ONE,
STRING_SCALAR,
type Subgraph,
subgraphValidationError,
SUBSCRIPTION,
Expand Down Expand Up @@ -453,6 +455,122 @@ describe('@openfed__subscriptionFilter tests', () => {
]);
});

test('that bypassIfValuesNull: true is preserved on the IN condition', () => {
const result = federateSubgraphsSuccess([subgraphB, subgraphBypass1], ROUTER_COMPATIBILITY_VERSION_ONE);
expect(result.success).toBe(true);
expect(result.fieldConfigurations).toStrictEqual([
{
argumentNames: [],
fieldName: 'field',
subscriptionFilterCondition: {
in: {
bypassIfValuesNull: true,
fieldPath: ['id'],
values: ['1'],
},
},
typeName: SUBSCRIPTION,
},
]);
});

test('that bypassIfValuesNull: false collapses to undefined', () => {
const result = federateSubgraphsSuccess([subgraphB, subgraphBypass2], ROUTER_COMPATIBILITY_VERSION_ONE);
expect(result.success).toBe(true);
expect(result.fieldConfigurations).toStrictEqual([
{
argumentNames: [],
fieldName: 'field',
subscriptionFilterCondition: {
in: {
fieldPath: ['id'],
values: ['1'],
},
},
typeName: SUBSCRIPTION,
},
]);
});

test('that omitting bypassIfValuesNull leaves the IN condition unchanged', () => {
const result = federateSubgraphsSuccess([subgraphB, subgraphC], ROUTER_COMPATIBILITY_VERSION_ONE);
expect(result.success).toBe(true);
expect(result.fieldConfigurations).toStrictEqual([
{
argumentNames: [],
fieldName: 'field',
subscriptionFilterCondition: {
in: {
fieldPath: ['id'],
values: ['1'],
},
},
typeName: SUBSCRIPTION,
},
]);
});

test('that bypassIfValuesNull is propagated through nested OR/IN conditions', () => {
const result = federateSubgraphsSuccess([subgraphB, subgraphBypass3], ROUTER_COMPATIBILITY_VERSION_ONE);
expect(result.success).toBe(true);
expect(result.fieldConfigurations).toStrictEqual([
{
argumentNames: [],
fieldName: 'field',
subscriptionFilterCondition: {
or: [
{
in: {
bypassIfValuesNull: true,
fieldPath: ['id'],
values: ['1'],
},
},
{
in: {
fieldPath: ['id'],
values: ['2'],
},
},
],
},
typeName: SUBSCRIPTION,
},
]);
});

test('that an error is returned if bypassIfValuesNull is not a Boolean', () => {
const result = federateSubgraphsFailure([subgraphB, subgraphBypass4], ROUTER_COMPATIBILITY_VERSION_ONE);
expect(result.errors).toHaveLength(1);
expect(result.errors).toStrictEqual([
invalidSubscriptionFilterDirectiveError('Subscription.field', [
subscriptionFieldConditionInvalidInputFieldErrorMessage(
'condition.IN',
[],
[],
[],
[invalidInputFieldTypeErrorMessage('condition.IN.bypassIfValuesNull', BOOLEAN, STRING_SCALAR)],
),
]),
]);
});

test('that an error is returned if bypassIfValuesNull is duplicated', () => {
const result = federateSubgraphsFailure([subgraphB, subgraphBypass5], ROUTER_COMPATIBILITY_VERSION_ONE);
expect(result.errors).toHaveLength(1);
expect(result.errors).toStrictEqual([
invalidSubscriptionFilterDirectiveError('Subscription.field', [
subscriptionFieldConditionInvalidInputFieldErrorMessage(
'condition.IN',
[],
['bypassIfValuesNull'],
[],
[],
),
]),
]);
});

test('that an entity can be defined as an extension in an EDG', () => {
const { federatedGraphSchema } = federateSubgraphsSuccess(
[subgraphQ, subgraphR],
Expand Down Expand Up @@ -659,23 +777,24 @@ const subgraphG: Subgraph = {
type Entity @key(fields: "id", resolvable: false) {
id: ID! @external
}

type Subscription {
field: Entity! @edfs__kafkaSubscribe(topics: ["employeeUpdated"]) @openfed__subscriptionFilter(condition: { IN: { fieldPath: "id", values: [1] } })
}

input openfed__SubscriptionFieldCondition {
bypassIfValuesNull: Boolean
fieldPath: String!
values: [openfed__SubscriptionFilterValue]!
}

input openfed__SubscriptionFilterCondition {
AND: [openfed__SubscriptionFilterCondition!]
IN: openfed__SubscriptionFieldCondition
NOT: openfed__SubscriptionFilterCondition
OR: [openfed__SubscriptionFilterCondition!]
}

scalar openfed__SubscriptionFilterValue
`),
};
Expand Down Expand Up @@ -927,3 +1046,80 @@ const subgraphR: Subgraph = {
}
`),
};

const subgraphBypass1: Subgraph = {
name: 'subgraph-bypass-1',
url: '',
definitions: parse(`
type Entity @key(fields: "id", resolvable: false) {
id: ID! @external
}

type Subscription {
field: Entity! @edfs__kafkaSubscribe(topics: ["employeeUpdated"]) @openfed__subscriptionFilter(condition: { IN: { fieldPath: "id", values: ["1"], bypassIfValuesNull: true } })
}
`),
};

const subgraphBypass2: Subgraph = {
name: 'subgraph-bypass-2',
url: '',
definitions: parse(`
type Entity @key(fields: "id", resolvable: false) {
id: ID! @external
}

type Subscription {
field: Entity! @edfs__kafkaSubscribe(topics: ["employeeUpdated"]) @openfed__subscriptionFilter(condition: { IN: { fieldPath: "id", values: ["1"], bypassIfValuesNull: false } })
}
`),
};

const subgraphBypass3: Subgraph = {
name: 'subgraph-bypass-3',
url: '',
definitions: parse(`
type Entity @key(fields: "id", resolvable: false) {
id: ID! @external
}

type Subscription {
field: Entity!
@edfs__kafkaSubscribe(topics: ["employeeUpdated"])
@openfed__subscriptionFilter(condition: {
OR: [
{ IN: { fieldPath: "id", values: ["1"], bypassIfValuesNull: true } },
{ IN: { fieldPath: "id", values: ["2"] } }
]
})
}
`),
};

const subgraphBypass4: Subgraph = {
name: 'subgraph-bypass-4',
url: '',
definitions: parse(`
type Entity @key(fields: "id", resolvable: false) {
id: ID! @external
}

type Subscription {
field: Entity! @edfs__kafkaSubscribe(topics: ["employeeUpdated"]) @openfed__subscriptionFilter(condition: { IN: { fieldPath: "id", values: ["1"], bypassIfValuesNull: "yes" } })
}
`),
};

const subgraphBypass5: Subgraph = {
name: 'subgraph-bypass-5',
url: '',
definitions: parse(`
type Entity @key(fields: "id", resolvable: false) {
id: ID! @external
}

type Subscription {
field: Entity! @edfs__kafkaSubscribe(topics: ["employeeUpdated"]) @openfed__subscriptionFilter(condition: { IN: { fieldPath: "id", values: ["1"], bypassIfValuesNull: true, bypassIfValuesNull: false } })
}
`),
};
1 change: 1 addition & 0 deletions composition/tests/v1/utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ export const OPENFED_SCOPE = ` scalar openfed__Scope`;

export const OPENFED_SUBSCRIPTION_FIELD_CONDITION = `
input openfed__SubscriptionFieldCondition {
bypassIfValuesNull: Boolean
fieldPath: String!
values: [openfed__SubscriptionFilterValue]!
}
Expand Down
25 changes: 18 additions & 7 deletions connect-go/gen/proto/wg/cosmo/node/v1/node.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading