Skip to content

Commit

Permalink
Merge pull request #5590 from neo4j/5586-subscriptions-using-id_in-op…
Browse files Browse the repository at this point in the history
…erator-parse-uuids-as-integers

Fix subscriptions with uid
  • Loading branch information
angrykoala authored Sep 24, 2024
2 parents 3062291 + e95db9c commit 883de3b
Show file tree
Hide file tree
Showing 4 changed files with 168 additions and 28 deletions.
5 changes: 5 additions & 0 deletions .changeset/flat-countries-play.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@neo4j/graphql": patch
---

Fix subscriptions with autogenerated uids #5586
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
* limitations under the License.
*/

import { int } from "neo4j-driver";
import { int, isInt } from "neo4j-driver";
import type { AttributeAdapter } from "../../../../../schema-model/attribute/model-adapters/AttributeAdapter";
import { getFilteringFn } from "../utils/get-filtering-fn";
import { multipleConditionsAggregationMap } from "../utils/multiple-conditions-aggregation-map";
Expand Down Expand Up @@ -69,42 +69,50 @@ export function filterByProperties<T>({
return true;
}

const isFloatOrStringOrIDAsString = (attributeAdapter: AttributeAdapter | undefined, value: string | number) =>
attributeAdapter?.typeHelper.isFloat() ||
attributeAdapter?.typeHelper.isString() ||
(attributeAdapter?.typeHelper.isID() && int(value).toString() !== value);
/** Checks if field is a string that needs to be parsed as int */
function shouldParseAsInt(attributeAdapter: AttributeAdapter | undefined, value: string | number) {
if (attributeAdapter?.typeHelper.isFloat() || attributeAdapter?.typeHelper.isString()) {
return false;
}

if (attributeAdapter?.typeHelper.isBigInt() || attributeAdapter?.typeHelper.isInt()) {
return true;
}

if (attributeAdapter?.typeHelper.isID()) {
return isInt(value);
}

return false;
}

const operatorMapOverrides = {
INCLUDES: (received: [string | number], filtered: string | number, fieldMeta: AttributeAdapter | undefined) => {
if (isFloatOrStringOrIDAsString(fieldMeta, filtered)) {
return received.some((v) => v === filtered);
if (shouldParseAsInt(fieldMeta, filtered)) {
const filteredAsNeo4jInteger = int(filtered);
return received.some((r) => int(r).equals(filteredAsNeo4jInteger));
}
// int/ bigint
const filteredAsNeo4jInteger = int(filtered);
return received.some((r) => int(r).equals(filteredAsNeo4jInteger));
return received.some((v) => v === filtered);
},
NOT_INCLUDES: (received: [string | number], filtered: string | number, fieldMeta: AttributeAdapter | undefined) => {
if (isFloatOrStringOrIDAsString(fieldMeta, filtered)) {
return !received.some((v) => v === filtered);
if (shouldParseAsInt(fieldMeta, filtered)) {
const filteredAsNeo4jInteger = int(filtered);
return !received.some((r) => int(r).equals(filteredAsNeo4jInteger));
}
// int/ bigint
const filteredAsNeo4jInteger = int(filtered);
return !received.some((r) => int(r).equals(filteredAsNeo4jInteger));
return !received.some((v) => v === filtered);
},
IN: (received: string | number, filtered: [string | number], fieldMeta: AttributeAdapter | undefined) => {
if (isFloatOrStringOrIDAsString(fieldMeta, received)) {
return filtered.some((v) => v === received);
if (shouldParseAsInt(fieldMeta, received)) {
const receivedAsNeo4jInteger = int(received);
return filtered.some((r) => int(r).equals(receivedAsNeo4jInteger));
}
// int/ bigint
const receivedAsNeo4jInteger = int(received);
return filtered.some((r) => int(r).equals(receivedAsNeo4jInteger));
return filtered.some((v) => v === received);
},
NOT_IN: (received: string | number, filtered: [string | number], fieldMeta: AttributeAdapter | undefined) => {
if (isFloatOrStringOrIDAsString(fieldMeta, received)) {
return !filtered.some((v) => v === received);
if (shouldParseAsInt(fieldMeta, received)) {
const receivedAsNeo4jInteger = int(received);
return !filtered.some((r) => int(r).equals(receivedAsNeo4jInteger));
}
// int/ bigint
const receivedAsNeo4jInteger = int(received);
return !filtered.some((r) => int(r).equals(receivedAsNeo4jInteger));
return !filtered.some((v) => v === received);
},
};
7 changes: 4 additions & 3 deletions packages/graphql/tests/e2e/setup/ws-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@
* limitations under the License.
*/

import ws from "ws";
import { asArray } from "@graphql-tools/utils";
import type { Client } from "graphql-ws";
import { createClient } from "graphql-ws";
import { EventEmitter } from "stream";
import ws from "ws";

const NEW_EVENT = "NEW_EVENT";

Expand Down Expand Up @@ -83,8 +84,8 @@ export class WebSocketTestClient {
this.events.push(value.data);
}
},
error: (err: Array<unknown>) => {
this.errors.push(...err);
error: (err: unknown) => {
this.errors.push(...asArray(err));
if (callback) {
// hack to be able to expect errors on bad subscriptions
// bc. resolve() happens before below reject()
Expand Down
126 changes: 126 additions & 0 deletions packages/graphql/tests/e2e/subscriptions/issues/5586.e2e.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/*
* Copyright (c) "Neo4j"
* Neo4j Sweden AB [http://neo4j.com]
*
* This file is part of Neo4j.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import supertest from "supertest";
import { Neo4jGraphQLSubscriptionsDefaultEngine } from "../../../../src/classes/subscription/Neo4jGraphQLSubscriptionsDefaultEngine";
import type { UniqueType } from "../../../utils/graphql-types";
import { TestHelper } from "../../../utils/tests-helper";
import type { TestGraphQLServer } from "../../setup/apollo-server";
import { ApolloTestServer } from "../../setup/apollo-server";
import { WebSocketTestClient } from "../../setup/ws-client";

describe("https:/neo4j/graphql/issues/5586", () => {
const testHelper = new TestHelper();
let server: TestGraphQLServer;
let wsClient: WebSocketTestClient;
let Entity: UniqueType;
let typeDefs: string;

beforeEach(async () => {
Entity = testHelper.createUniqueType("Entity");

typeDefs = /* GraphQL */ `
type ${Entity} {
id: ID! @id @unique
name: String
}
`;

const neoSchema = await testHelper.initNeo4jGraphQL({
typeDefs,
features: {
subscriptions: new Neo4jGraphQLSubscriptionsDefaultEngine(),
},
});
// eslint-disable-next-line @typescript-eslint/require-await
server = new ApolloTestServer(neoSchema, async ({ req }) => ({
sessionConfig: {
database: testHelper.database,
},
token: req.headers.authorization,
}));
await server.start();

wsClient = new WebSocketTestClient(server.wsPath);
});

afterEach(async () => {
await wsClient.close();

await testHelper.close();
await server.close();
});

test("connect via create subscription sends events both ways", async () => {
await testHelper.executeCypher(
`CREATE(:${Entity} {id: "7fab55b1-6cd2-489d-92ca-f4944478d127", name: "original"})`
);

await wsClient.subscribe(/* GraphQL */ `
subscription {
${Entity.operations.subscribe.updated}(where: { id_IN: ["7fab55b1-6cd2-489d-92ca-f4944478d127"] }) {
${Entity.operations.subscribe.payload.updated} {
id
name
}
event
timestamp
previousState {
id
name
}
}
}
`);

await supertest(server.path)
.post("")
.send({
query: /* GraphQL */ `
mutation {
${Entity.operations.update}(update: { name: "new" }) {
${Entity.plural} {
name
id
}
}
}
`,
})
.expect(200);

await wsClient.waitForEvents(1);

expect(wsClient.errors).toEqual([]);
expect(wsClient.events).toHaveLength(1);
expect(wsClient.events).toIncludeSameMembers([
{
[Entity.operations.subscribe.updated]: {
event: "UPDATE",
previousState: { id: "7fab55b1-6cd2-489d-92ca-f4944478d127", name: "original" },
timestamp: expect.toBeNumber(),
[Entity.operations.subscribe.payload.updated]: {
id: "7fab55b1-6cd2-489d-92ca-f4944478d127",
name: "new",
},
},
},
]);
});
});

0 comments on commit 883de3b

Please sign in to comment.