Skip to content

Commit c37c23b

Browse files
committed
Fix filterBy expression handling
1 parent 9d41841 commit c37c23b

File tree

12 files changed

+213
-33
lines changed

12 files changed

+213
-33
lines changed

.changeset/icy-cities-enjoy.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@graphql-mesh/types': patch
3+
'@graphql-mesh/utils': patch
4+
---
5+
6+
Fix `filterBy` expression handling

e2e/json-schema-subscriptions/__snapshots__/json-schema-subscriptions.test.ts.snap

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,25 +63,39 @@ type Query @extraSchemaDefinitionDirective(directives: {transport: [{subgraph: "
6363
}
6464
6565
type query_todos_items @join__type(graph: API) {
66-
id: Int
66+
id: String
6767
name: String
6868
content: String
6969
}
7070
7171
type Mutation @join__type(graph: API) {
72-
addTodo(input: mutationInput_addTodo_input_Input): Todo @httpOperation(subgraph: "API", path: "/todo", httpMethod: POST)
72+
addTodo(input: mutationInput_addTodo_input_Input): Todo @httpOperation(subgraph: "API", path: "/todo", httpMethod: PUT)
73+
updateTodo(input: mutationInput_updateTodo_input_Input): mutation_updateTodo @httpOperation(subgraph: "API", path: "/todo", httpMethod: PATCH)
7374
}
7475
75-
type Todo @example(subgraph: "API", value: "{\\"id\\":0,\\"name\\":\\"TodoName\\",\\"content\\":\\"TodoContent\\"}") @join__type(graph: API) {
76-
id: Int
76+
type Todo @example(subgraph: "API", value: "{\\"id\\":\\"0\\",\\"name\\":\\"TodoName\\",\\"content\\":\\"TodoContent\\"}") @join__type(graph: API) {
77+
id: String
78+
name: String
79+
content: String
80+
}
81+
82+
type mutation_updateTodo @example(subgraph: "API", value: "{\\"id\\":\\"0\\",\\"name\\":\\"TodoName\\",\\"content\\":\\"TodoContent\\"}") @join__type(graph: API) {
83+
id: String
7784
name: String
7885
content: String
7986
}
8087
8188
type Subscription @join__type(graph: API) {
8289
"""PubSub Topic: webhook:post:/webhooks/todo_added"""
83-
todoAddedFromSource: Todo @pubsubOperation(subgraph: "API", pubsubTopic: "webhook:post:/webhooks/todo_added")
90+
todoAddedFromSource: subscription_todoAddedFromSource @pubsubOperation(subgraph: "API", pubsubTopic: "webhook:post:/webhooks/todo_added")
8491
todoAddedFromExtensions: Todo @resolveTo(pubsubTopic: "webhook:post:/webhooks/todo_added") @additionalField
92+
todoUpdatedFromExtensions(id: ID!): Todo @resolveTo(pubsubTopic: "webhook:post:/webhooks/todo_updated", filterBy: "root.id === args.id") @additionalField
93+
}
94+
95+
type subscription_todoAddedFromSource @example(subgraph: "API", value: "{\\"id\\":\\"0\\",\\"name\\":\\"TodoName\\",\\"content\\":\\"TodoContent\\"}") @join__type(graph: API) {
96+
id: String
97+
name: String
98+
content: String
8599
}
86100
87101
enum HTTPMethod @join__type(graph: API) {
@@ -100,5 +114,11 @@ input mutationInput_addTodo_input_Input @example(subgraph: "API", value: "{\\"na
100114
name: String
101115
content: String
102116
}
117+
118+
input mutationInput_updateTodo_input_Input @example(subgraph: "API", value: "{\\"id\\":\\"0\\",\\"name\\":\\"TodoName\\",\\"content\\":\\"TodoContent\\"}") @join__type(graph: API) {
119+
id: String
120+
name: String
121+
content: String
122+
}
103123
"
104124
`;

e2e/json-schema-subscriptions/json-schema-subscriptions.test.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,3 +93,78 @@ it('should compose the appropriate schema', async () => {
9393
});
9494
});
9595
});
96+
97+
it('should subscribe to todoUpdatedFromExtensions', async () => {
98+
const servePort = await getAvailablePort();
99+
const api = await service('api', { servePort });
100+
const { output } = await compose({ output: 'graphql', services: [api] });
101+
const { hostname, port, execute } = await serve({ supergraph: output, port: servePort });
102+
103+
// Add a todo to update and get the id
104+
const result = await execute({
105+
query: /* GraphQL */ `
106+
mutation AddTodo {
107+
addTodo(input: { name: "Shopping", content: "Buy Milk" }) {
108+
id
109+
name
110+
content
111+
}
112+
}
113+
`,
114+
});
115+
const todoId = result.data.addTodo.id;
116+
117+
const sse = createClient({
118+
url: `http://${hostname}:${port}/graphql`,
119+
retryAttempts: 0,
120+
fetchFn: fetch,
121+
});
122+
123+
const sub = sse.iterate({
124+
query: /* GraphQL */ `
125+
subscription todoUpdatedFromExtensions($id: ID!) {
126+
todoUpdatedFromExtensions(id: $id) {
127+
name
128+
content
129+
}
130+
}
131+
`,
132+
variables: { id: todoId },
133+
});
134+
135+
await expect(
136+
execute({
137+
query: /* GraphQL */ `
138+
mutation UpdateTodo {
139+
updateTodo(input: { id: "${todoId}", name: "Shopping", content: "Buy Eggs" }) {
140+
name
141+
content
142+
}
143+
}
144+
`,
145+
}),
146+
).resolves.toMatchInlineSnapshot(`
147+
{
148+
"data": {
149+
"updateTodo": {
150+
"content": "Buy Eggs",
151+
"name": "Shopping",
152+
},
153+
},
154+
}
155+
`);
156+
157+
for await (const msg of sub) {
158+
expect(msg).toMatchInlineSnapshot(`
159+
{
160+
"data": {
161+
"todoUpdatedFromExtensions": {
162+
"content": "Buy Eggs",
163+
"name": "Shopping",
164+
},
165+
},
166+
}
167+
`);
168+
break;
169+
}
170+
});

e2e/json-schema-subscriptions/mesh.config.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export const composeConfig = defineComposeConfig({
2727
type: OperationTypeNode.MUTATION,
2828
field: 'addTodo',
2929
path: '/todo',
30-
method: 'POST',
30+
method: 'PUT',
3131
requestSample: './addTodo.json',
3232
responseSample: './todo.json',
3333
responseTypeName: 'Todo',
@@ -39,6 +39,15 @@ export const composeConfig = defineComposeConfig({
3939
responseSample: './todo.json',
4040
responseTypeName: 'Todo',
4141
},
42+
{
43+
type: OperationTypeNode.MUTATION,
44+
field: 'updateTodo',
45+
path: '/todo',
46+
method: 'PATCH',
47+
requestSample: './todo.json',
48+
responseSample: './todo.json',
49+
responseTypeName: 'Todo',
50+
},
4251
],
4352
}),
4453
},
@@ -54,6 +63,11 @@ export const composeConfig = defineComposeConfig({
5463
5564
type Subscription {
5665
todoAddedFromExtensions: Todo @resolveTo(pubsubTopic: "webhook:post:/webhooks/todo_added")
66+
todoUpdatedFromExtensions(id: ID!): Todo
67+
@resolveTo(
68+
pubsubTopic: "webhook:post:/webhooks/todo_updated"
69+
filterBy: "root.id === args.id"
70+
)
5771
}
5872
`,
5973
});

e2e/json-schema-subscriptions/services/api.ts

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@ const app = createRouter<FetchEvent>()
1616
})
1717
.route({
1818
path: '/todo',
19-
method: 'POST',
19+
method: 'PUT',
2020
async handler(request, { waitUntil }) {
2121
const reqBody = await request.json();
2222
const todo = {
23-
id: todos.length,
23+
id: todos.length.toString(),
2424
...reqBody,
2525
};
2626
todos.push(todo);
@@ -49,6 +49,42 @@ const app = createRouter<FetchEvent>()
4949
);
5050
return Response.json(todo);
5151
},
52+
})
53+
.route({
54+
path: '/todo',
55+
method: 'PATCH',
56+
async handler(request, { waitUntil }) {
57+
const reqBody = await request.json();
58+
const todo = {
59+
...todos[reqBody.id],
60+
...reqBody,
61+
};
62+
todos[reqBody.id] = todo;
63+
const port = opts.getPort(true);
64+
waitUntil(
65+
getLocalHostName(port).then(hostname =>
66+
fetch(`http://${hostname}:${port}/webhooks/todo_updated`, {
67+
method: 'POST',
68+
headers: {
69+
'Content-Type': 'application/json',
70+
},
71+
body: JSON.stringify(todo),
72+
})
73+
.then(res =>
74+
res.text().then(resText =>
75+
console.log('Webhook payload sent', {
76+
status: res.status,
77+
statusText: res.statusText,
78+
body: resText,
79+
headers: Object.fromEntries(res.headers.entries()),
80+
}),
81+
),
82+
)
83+
.catch(err => console.error('Webhook payload failed', err)),
84+
),
85+
);
86+
return Response.json(todo);
87+
},
5288
});
5389

5490
const port = opts.getServicePort('api', true);
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"id": 0,
2+
"id": "0",
33
"name": "TodoName",
44
"content": "TodoContent"
55
}

e2e/json-schema-subscriptions/todos.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[
22
{
3-
"id": 0,
3+
"id": "0",
44
"name": "TodoName",
55
"content": "TodoContent"
66
}

packages/legacy/types/src/config-schema.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,10 +110,12 @@
110110
"description": "Flag to indicate lazyConnect value for Redis client.\n\n@default: true"
111111
},
112112
"dnsLookupAsIs": {
113-
"type": "boolean"
113+
"type": "boolean",
114+
"description": "Needed for TLS connections to Redis Cluster (especially when using AWS Elasticache Clusters with TLS).\n\n@see https://github.com/redis/ioredis?tab=readme-ov-file#special-note-aws-elasticache-clusters-with-tls"
114115
},
115116
"tls": {
116-
"type": "boolean"
117+
"type": "boolean",
118+
"description": "Enable TLS for Redis Cluster connections. Required for AWS Elasticache Clusters with TLS.\n\n@see https://github.com/redis/ioredis?tab=readme-ov-file#special-note-aws-elasticache-clusters-with-tls"
117119
}
118120
},
119121
"required": ["startupNodes"]

packages/legacy/types/src/config.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1895,7 +1895,17 @@ export interface RedisConfigCluster {
18951895
* @default: true
18961896
*/
18971897
lazyConnect?: boolean;
1898+
/**
1899+
* Needed for TLS connections to Redis Cluster (especially when using AWS Elasticache Clusters with TLS).
1900+
*
1901+
* @see https://github.com/redis/ioredis?tab=readme-ov-file#special-note-aws-elasticache-clusters-with-tls
1902+
*/
18981903
dnsLookupAsIs?: boolean;
1904+
/**
1905+
* Enable TLS for Redis Cluster connections. Required for AWS Elasticache Clusters with TLS.
1906+
*
1907+
* @see https://github.com/redis/ioredis?tab=readme-ov-file#special-note-aws-elasticache-clusters-with-tls
1908+
*/
18991909
tls?: boolean;
19001910
}
19011911
export interface PubSubConfig {

packages/legacy/utils/src/resolve-additional-resolvers.ts

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { process } from '@graphql-mesh/cross-helpers';
1414
import type { MeshContext } from '@graphql-mesh/runtime';
1515
import { stringInterpolator } from '@graphql-mesh/string-interpolation';
1616
import type { ImportFn, MeshPubSub, YamlConfig } from '@graphql-mesh/types';
17-
import type { IResolvers } from '@graphql-tools/utils';
17+
import type { IResolvers, MaybePromise } from '@graphql-tools/utils';
1818
import { parseSelectionSet } from '@graphql-tools/utils';
1919
import { loadFromModuleExportExpression } from './load-from-module-export-expression.js';
2020
import { withFilter } from './with-filter.js';
@@ -159,22 +159,39 @@ export function resolveAdditionalResolversWithoutImport(
159159
baseOptions.valuesFromResults = generateValuesFromResults(additionalResolver.result);
160160
}
161161
if ('pubsubTopic' in additionalResolver) {
162+
const pubsubTopic = additionalResolver.pubsubTopic;
163+
let subscribeFn = function subscriber(
164+
root: any,
165+
args: Record<string, any>,
166+
context: MeshContext,
167+
info: GraphQLResolveInfo,
168+
): MaybePromise<AsyncIterator<any>> {
169+
const resolverData = { root, args, context, info, env: process.env };
170+
const topic = stringInterpolator.parse(pubsubTopic, resolverData);
171+
return (context.pubsub || pubsub).asyncIterator(topic)[Symbol.asyncIterator]();
172+
};
173+
if (additionalResolver.filterBy) {
174+
let filterFunction: any;
175+
try {
176+
// eslint-disable-next-line no-new-func
177+
filterFunction = new Function(
178+
'root',
179+
'args',
180+
'context',
181+
'info',
182+
`return ${additionalResolver.filterBy};`,
183+
);
184+
} catch (e) {
185+
throw new Error(
186+
`Error while parsing filterBy expression "${additionalResolver.filterBy}" in additional subscription resolver: ${e.message}`,
187+
);
188+
}
189+
subscribeFn = withFilter(subscribeFn, filterFunction);
190+
}
162191
return {
163192
[additionalResolver.targetTypeName]: {
164193
[additionalResolver.targetFieldName]: {
165-
subscribe: withFilter(
166-
(root, args, context: MeshContext, info) => {
167-
const resolverData = { root, args, context, info, env: process.env };
168-
const topic = stringInterpolator.parse(additionalResolver.pubsubTopic, resolverData);
169-
return (pubsub || context.pubsub).asyncIterator(topic)[Symbol.asyncIterator]();
170-
},
171-
(root, args, context, info) => {
172-
return additionalResolver.filterBy
173-
? // eslint-disable-next-line no-new-func
174-
new Function(`return ${additionalResolver.filterBy}`)()
175-
: true;
176-
},
177-
),
194+
subscribe: subscribeFn,
178195
resolve: (payload: any) => {
179196
if (baseOptions.valuesFromResults) {
180197
return baseOptions.valuesFromResults(payload);

0 commit comments

Comments
 (0)