Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add naming customisation options for introspector #4076

Open
wants to merge 4 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/neat-insects-pretend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@neo4j/introspector": minor
---

Added options to customise schema naming conventions to introspector
2 changes: 1 addition & 1 deletion packages/graphql/tests/performance/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,6 @@
"neo4j-driver": "^5.8.0"
},
"dependencies": {
"@neo4j/graphql": "link:../../.."
"@neo4j/graphql": "file:../../.."
}
}
58 changes: 57 additions & 1 deletion packages/introspector/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,16 +97,72 @@ You can introspect the schema and then transform it to any desired format.
Example:

```js
import { toGenericStruct } from "@neo4j/introspector";
import { toGenericStruct, graphqlFormatter } from "@neo4j/introspector";
import neo4j from "neo4j-driver";

const driver = neo4j.driver("neo4j://localhost:7687", neo4j.auth.basic("neo4j", "password"));

const sessionFactory = () => driver.session({ defaultAccessMode: neo4j.session.READ });

async function main() {
const readonly = true; // We don't want to expose mutations in this case
const genericStruct = await toGenericStruct(sessionFactory);

// Programmatically transform to what you need.

// create type definitions for gql from the generic string.
const typeDefs = graphqlFormatter(genericStruct, readonly);

const neoSchema = new Neo4jGraphQL({ typeDefs, driver });
const server = new ApolloServer({
schema: await neoSchema.getSchema(),
});

await startStandaloneServer(server, {
context: async ({ req }) => ({ req }),
});
}

main();
```

### Sanitize GraphQL type names

You can sanitize the GraphQL type names by passing functions using the `sanitizeNodeLabels` and `sanitizeRelationshipTypes` options.

```js
import { toGenericStruct, graphqlFormatter } from "@neo4j/introspector";
import neo4j from "neo4j-driver";

const driver = neo4j.driver("neo4j://localhost:7687", neo4j.auth.basic("neo4j", "password"));

const sessionFactory = () => driver.session({ defaultAccessMode: neo4j.session.READ });

async function main() {
const readonly = true; // We don't want to expose mutations in this case
const genericStruct = await toGenericStruct(sessionFactory);

// Programmatically transform to what you need.

// create type definitions for gql from the generic string.
const typeDefs = graphqlFormatter(genericStruct, readonly, {
sanitizeNodeLabels: (node) =>
node.labels
.sort((a, b) => a.length - b.length)[0] // Take the shortest label
.replace(/[^a-zA-Z0-9]/g, "") // Remove all non-alphanumeric characters
.replace(/^graph_node_prefix__/g, ""), // Remove a prefix
// Remove all non-alphanumeric characters from the relationship types
sanitizeRelationshipTypes: (type) => type.replace(/[^a-zA-Z0-9]/g, ""),
});

const neoSchema = new Neo4jGraphQL({ typeDefs, driver });
const server = new ApolloServer({
schema: await neoSchema.getSchema(),
});

await startStandaloneServer(server, {
context: async ({ req }) => ({ req }),
});
}

main();
Expand Down
1 change: 1 addition & 0 deletions packages/introspector/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import type { Session } from "neo4j-driver";
import graphqlFormatter from "./transforms/neo4j-graphql";
import toInternalStruct from "./to-internal-struct";
import type { Neo4jStruct } from "./types";
export { graphqlFormatter };

export async function toGenericStruct(sessionFactory: () => Session): Promise<Neo4jStruct> {
return toInternalStruct(sessionFactory);
Expand Down
33 changes: 26 additions & 7 deletions packages/introspector/src/transforms/neo4j-graphql/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,25 @@ import { RelationshipPropertiesDirective } from "./directives/RelationshipProper
import createRelationshipFields from "./utils/create-relationship-fields";
import generateGraphQLSafeName from "./utils/generate-graphql-safe-name";
import nodeKey from "../../utils/node-key";
import type Node from "../../classes/Node";

type GraphQLNodeMap = {
[key: string]: GraphQLNode;
};

export default function graphqlFormatter(neo4jStruct: Neo4jStruct, readonly = false): string {
type FormatterOptions = {
getNodeLabel?: (node: Node) => string;
sanitizeRelType?: (relType: string) => string;
};

export default function graphqlFormatter(
neo4jStruct: Neo4jStruct,
readonly = false,
options: FormatterOptions = {}
): string {
const { nodes, relationships } = neo4jStruct;
const bareNodes = transformNodes(nodes);
const withRelationships = hydrateWithRelationships(bareNodes, relationships);
const bareNodes = transformNodes(nodes, options);
const withRelationships = hydrateWithRelationships(bareNodes, relationships, options);
const sorted = Object.keys(withRelationships).sort((a, b) => {
return withRelationships[a].typeName > withRelationships[b].typeName ? 1 : -1;
});
Expand All @@ -46,17 +56,20 @@ export default function graphqlFormatter(neo4jStruct: Neo4jStruct, readonly = fa
return sortedWithRelationships.join("\n\n");
}

function transformNodes(nodes: NodeMap): GraphQLNodeMap {
function transformNodes(nodes: NodeMap, options: FormatterOptions = {}): GraphQLNodeMap {
const out = {};
const takenTypeNames: string[] = [];
Object.keys(nodes).forEach((nodeType) => {
// No labels, skip
if (!nodeType) {
return;
}

const neo4jNode = nodes[nodeType];

const neo4jNodeKey = nodeKey(neo4jNode.labels);
const mainLabel = neo4jNode.labels[0];

const mainLabel = options.getNodeLabel ? options.getNodeLabel(neo4jNode) : neo4jNode.labels[0];
const typeName = generateGraphQLSafeName(mainLabel);

const uniqueTypeName = uniqueString(typeName, takenTypeNames);
Expand All @@ -79,7 +92,11 @@ function transformNodes(nodes: NodeMap): GraphQLNodeMap {
return out;
}

function hydrateWithRelationships(nodes: GraphQLNodeMap, rels: RelationshipMap): GraphQLNodeMap {
function hydrateWithRelationships(
nodes: GraphQLNodeMap,
rels: RelationshipMap,
options: FormatterOptions = {}
): GraphQLNodeMap {
Object.entries(rels).forEach(([relType, rel]) => {
let relInterfaceName: string;

Expand All @@ -94,12 +111,14 @@ function hydrateWithRelationships(nodes: GraphQLNodeMap, rels: RelationshipMap):
relTypePropertiesFields.forEach((f) => relInterfaceNode.addField(f));
nodes[relInterfaceName] = relInterfaceNode;
}
// console.dir(rel, { depth: 7 });
rel.paths.forEach((path) => {
const { fromField, toField } = createRelationshipFields(
nodes[path.fromTypeId].typeName,
nodes[path.toTypeId].typeName,
relType,
relInterfaceName
relInterfaceName,
options.sanitizeRelType
);
nodes[path.fromTypeId].addField(fromField);
nodes[path.toTypeId].addField(toField);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,18 @@ export default function createRelationshipFields(
fromTypeName: string,
toTypeName: string,
relType: string,
propertiesTypeName?: string
propertiesTypeName?: string,
sanitizeRelType?: (relType: string) => string
): { fromField: NodeField; toField: NodeField } {
const fromField = new NodeField(
generateRelationshipFieldName(relType, fromTypeName, toTypeName, "OUT"),
generateRelationshipFieldName(relType, fromTypeName, toTypeName, "OUT", sanitizeRelType),
`[${toTypeName}!]!`
);
const fromDirective = new RelationshipDirective(relType, "OUT", propertiesTypeName);
fromField.addDirective(fromDirective);

const toField = new NodeField(
generateRelationshipFieldName(relType, fromTypeName, toTypeName, "IN"),
generateRelationshipFieldName(relType, fromTypeName, toTypeName, "IN", sanitizeRelType),
`[${fromTypeName}!]!`
);
const toDirective = new RelationshipDirective(relType, "IN", propertiesTypeName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,10 @@ export default function inferRelationshipFieldName(
relType: string,
fromType: string,
toType: string,
direction: Direction
direction: Direction,
sanitizeRelType: (relType: string) => string = (relType) => relType.replace(/[\s/()\\`]/g, "")
): string {
const sanitizedRelType = relType.replaceAll(/[\s/()\\`]/g, "");
const sanitizedRelType = sanitizeRelType(relType);
if (direction === "OUT") {
return camelcase(sanitizedRelType + pluralize(toType));
}
Expand Down
12 changes: 6 additions & 6 deletions packages/introspector/tests/integration/graphql/graphs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ describe("GraphQL - Infer Schema on graphs", () => {
driver = await createDriver();
const cSession = driver.session({ defaultAccessMode: neo4j.session.WRITE });
try {
await cSession.writeTransaction((tx) => tx.run(`CREATE DATABASE ${dbName} WAIT`));
await cSession.executeWrite((tx) => tx.run(`CREATE DATABASE ${dbName} WAIT`));
} catch (e) {
if (e instanceof Error) {
if (
Expand Down Expand Up @@ -69,7 +69,7 @@ describe("GraphQL - Infer Schema on graphs", () => {
if (MULTIDB_SUPPORT) {
const cSession = driver.session({ defaultAccessMode: neo4j.session.WRITE });
try {
await cSession.writeTransaction((tx) => tx.run(`DROP DATABASE ${dbName}`));
await cSession.executeWrite((tx) => tx.run(`DROP DATABASE ${dbName}`));
} catch (e) {
// ignore
}
Expand All @@ -86,7 +86,7 @@ describe("GraphQL - Infer Schema on graphs", () => {

const nodeProperties = { title: "Forrest Gump", name: "Glenn Hysén" };
const wSession = driver.session({ defaultAccessMode: neo4j.session.WRITE, database: dbName });
await wSession.writeTransaction((tx) =>
await wSession.executeWrite((tx) =>
tx.run(
`CREATE (m:Movie {title: $props.title})
CREATE (a:Actor {name: $props.name})
Expand Down Expand Up @@ -125,7 +125,7 @@ describe("GraphQL - Infer Schema on graphs", () => {

const nodeProperties = { title: "Forrest Gump", name: "Glenn Hysén" };
const wSession = driver.session({ defaultAccessMode: neo4j.session.WRITE, database: dbName });
await wSession.writeTransaction((tx) =>
await wSession.executeWrite((tx) =>
tx.run(
`CREATE (m:Movie {title: $props.title})
CREATE (p:Play:Theater {title: $props.title})
Expand Down Expand Up @@ -192,7 +192,7 @@ describe("GraphQL - Infer Schema on graphs", () => {
int: neo4j.int(1),
};
const wSession = driver.session({ defaultAccessMode: neo4j.session.WRITE, database: dbName });
await wSession.writeTransaction((tx) =>
await wSession.executeWrite((tx) =>
tx.run(
`CREATE (m:Movie {title: $props.title})
CREATE (a:Actor {name: $props.name})
Expand Down Expand Up @@ -251,7 +251,7 @@ describe("GraphQL - Infer Schema on graphs", () => {
roles: ["Footballer", "Drunken man on the street"],
};
const wSession = driver.session({ defaultAccessMode: neo4j.session.WRITE, database: dbName });
await wSession.writeTransaction((tx) =>
await wSession.executeWrite((tx) =>
tx.run(
`CREATE (m:\`Movie-Label\` {title: $props.title})
CREATE (a:\`Actor-Label\` {name: $props.name})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ describe("GraphQL - Infer Schema on graphs", () => {
driver = await createDriver();
const cSession = driver.session({ defaultAccessMode: neo4j.session.WRITE });
try {
await cSession.writeTransaction((tx) => tx.run(`CREATE DATABASE ${dbName} WAIT`));
await cSession.executeWrite((tx) => tx.run(`CREATE DATABASE ${dbName} WAIT`));
} catch (e) {
if (e instanceof Error) {
if (
Expand Down Expand Up @@ -69,7 +69,7 @@ describe("GraphQL - Infer Schema on graphs", () => {
if (MULTIDB_SUPPORT) {
const cSession = driver.session({ defaultAccessMode: neo4j.session.WRITE });
try {
await cSession.writeTransaction((tx) => tx.run(`DROP DATABASE ${dbName}`));
await cSession.executeWrite((tx) => tx.run(`DROP DATABASE ${dbName}`));
} catch (e) {
// ignore
}
Expand All @@ -85,7 +85,7 @@ describe("GraphQL - Infer Schema on graphs", () => {
}

const wSession = driver.session({ defaultAccessMode: neo4j.session.WRITE, database: dbName });
await wSession.writeTransaction((tx) =>
await wSession.executeWrite((tx) =>
tx.run("CREATE (a:Wurst) -[:```MATCH (n) DETACH DELETE n //`] -> (:Salat)")
);
const bm = wSession.lastBookmark();
Expand Down