Skip to content

Commit

Permalink
Allow Special Characters in Schema This to support any graph data tha…
Browse files Browse the repository at this point in the history
…t has special/invalid characters (#41)

Changes
- get data using OpenCypher queries
  - add backticks to labels to escape special characters
- creates graphQL schema
  - replaces invalid characters with something else that works (ex. `_cn_`, `_dot_`, `_hy_`)
  - adds alias directives with the original label
- querying in AppSync which converts graphQL requests to OpenCypher queries
  - uses the labels in the alias directives to create query with original labels
  - add backticks when creating query to escape special characters

Co-authored-by: sophiadt <sophiadnth@gmail.com>
  • Loading branch information
andreachild and sophiadt authored Nov 20, 2024
1 parent 592ab86 commit 2d6c08c
Show file tree
Hide file tree
Showing 23 changed files with 482 additions and 245 deletions.
6 changes: 2 additions & 4 deletions src/NeptuneSchema.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,7 @@ const schema = {
};

function sanitize(text) {
// TODO implement sanitization logic
// placeholder for sanitization of query text that cannot be parameterized
return text;
return `\`${text}\``;
}

/**
Expand Down Expand Up @@ -446,7 +444,7 @@ async function getNeptuneSchema() {
await getEdgesDirections();

await getEdgesDirectionsCardinality();

return JSON.stringify(schema, null, 2);
}

Expand Down
161 changes: 108 additions & 53 deletions src/graphdb.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ function checkForDuplicateNames(schema) {
}


// Write a function that takes an input a string and return the string lowercase except the first charachter uppercase
// Write a function that takes an input a string and return the string lowercase except the first character uppercase
function toPascalCase (str) {
let r = '';
if (changeCase) {
Expand All @@ -52,6 +52,25 @@ function toPascalCase (str) {
return r.trim();
}

// Changes every instance of invalid characters in the given label with the following abbreviations
function cleanseLabel(label) {
return label
.replaceAll("!", "_ex_")
.replaceAll("$", "_dol_")
.replaceAll("&", "_amp_")
.replaceAll("(", "_op_")
.replaceAll(")", "_cp_")
.replaceAll(".", "_dot_")
.replaceAll(":", "_cn_")
.replaceAll("=", "_eq_")
.replaceAll("@", "_at_")
.replaceAll("[", "_os_")
.replaceAll("]", "_cs_")
.replaceAll("{", "_oc_")
.replaceAll("|", "_vb_")
.replaceAll("}", "_cc_")
.replaceAll("-", "_hy_");
}

function graphDBInferenceSchema (graphbSchema, addMutations) {
let r = '';
Expand All @@ -61,65 +80,77 @@ function graphDBInferenceSchema (graphbSchema, addMutations) {

gdbs.nodeStructures.forEach(node => {
// node type
let nodeCase = node.label;
let nodeCase = cleanseLabel(node.label);
if (changeCase) {
nodeCase = toPascalCase(node.label);
nodeCase = toPascalCase(nodeCase);
}

if (node.label !== nodeCase) {
r += `type ${nodeCase} @alias(property:"${node.label}") {\n`;
} else {
}
else {
r += `type ${nodeCase} {\n`;
}

r += '\t_id: ID! @id\n';

node.properties.forEach(property => {
if (property.name == 'id')
if (property.name === 'id') {
r+= `\tid: ID\n`;
else
r+= `\t${property.name}: ${property.type}\n`;
}
else {
let propertyCase = cleanseLabel(property.name);
let alias = '';
if (property.name !== propertyCase) {
alias = ` @alias(property: "${property.name}")`;
}
r+= `\t${propertyCase}: ${property.type}${alias}\n`;
}
});

let edgeTypes = [];
gdbs.edgeStructures.forEach(edge => {
edge.directions.forEach(direction => {
let fromCase = toPascalCase(direction.from);
let toCase = toPascalCase(direction.to);
let edgeCase = toPascalCase(edge.label);
if (direction.from == node.label && direction.to == node.label){
if (direction.relationship == 'MANY-MANY') {
let fromCase = toPascalCase(cleanseLabel(direction.from));
let toCase = toPascalCase(cleanseLabel(direction.to));
let edgeCase = toPascalCase(cleanseLabel(edge.label));

if (direction.from === node.label && direction.to === node.label){
if (direction.relationship === 'MANY-MANY') {
r += `\t${nodeCase.toLocaleLowerCase() + edgeCase}sOut(filter: ${nodeCase}Input, options: Options): [${nodeCase}] @relationship(edgeType:"${edge.label}", direction:OUT)\n`;
r += `\t${nodeCase.toLocaleLowerCase() + edgeCase}sIn(filter: ${nodeCase}Input, options: Options): [${nodeCase}] @relationship(edgeType:"${edge.label}", direction:IN)\n`;
r += `\t${nodeCase.toLocaleLowerCase() + edgeCase}sIn(filter: ${nodeCase}Input, options: Options): [${nodeCase}] @relationship(edgeType:"${edge.label}", direction:IN)\n`;
}
if (direction.relationship == 'ONE-ONE') {
if (direction.relationship === 'ONE-ONE') {
r += `\t${nodeCase.toLocaleLowerCase() + edgeCase}Out: ${nodeCase} @relationship(edgeType:"${edge.label}", direction:OUT)\n`;
r += `\t${nodeCase.toLocaleLowerCase() + edgeCase}In: ${nodeCase} @relationship(edgeType:"${edge.label}", direction:IN)\n`;
}
if (!edgeTypes.includes(edge.label))
edgeTypes.push(edge.label);
}

if (direction.from == node.label && direction.to != node.label){
if (direction.relationship == 'MANY-MANY') {
r += `\t${toCase.toLocaleLowerCase() + edgeCase}sOut(filter: ${toCase}Input, options: Options): [${toCase}] @relationship(edgeType:"${edge.label}", direction:OUT)\n`
if (direction.from === node.label && direction.to !== node.label){
if (direction.relationship === 'MANY-MANY') {
r += `\t${toCase.toLocaleLowerCase() + edgeCase}sOut(filter: ${toCase}Input, options: Options): [${toCase}] @relationship(edgeType:"${edge.label}", direction:OUT)\n`;
}
if (direction.relationship == 'ONE-MANY') {
r += `\t${toCase.toLocaleLowerCase() + edgeCase}sOut(filter: ${toCase}Input, options: Options): [${toCase}] @relationship(edgeType:"${edge.label}", direction:OUT)\n`
if (direction.relationship === 'ONE-MANY') {
r += `\t${toCase.toLocaleLowerCase() + edgeCase}sOut(filter: ${toCase}Input, options: Options): [${toCase}] @relationship(edgeType:"${edge.label}", direction:OUT)\n`;
}
if (direction.relationship == 'MANY-ONE') {
r += `\t${toCase.toLocaleLowerCase() + edgeCase}Out: ${toCase} @relationship(edgeType:"${edge.label}", direction:OUT)\n`
if (direction.relationship === 'MANY-ONE') {
r += `\t${toCase.toLocaleLowerCase() + edgeCase}Out: ${toCase} @relationship(edgeType:"${edge.label}", direction:OUT)\n`;
}
if (!edgeTypes.includes(edge.label))
edgeTypes.push(edge.label);
}

if (direction.from != node.label && direction.to == node.label){
if (direction.relationship == 'MANY-MANY') {
if (direction.from !== node.label && direction.to === node.label){
if (direction.relationship === 'MANY-MANY') {
r += `\t${fromCase.toLocaleLowerCase() + edgeCase}sIn(filter: ${fromCase}Input, options: Options): [${fromCase}] @relationship(edgeType:"${edge.label}", direction:IN)\n`
}
if (direction.relationship == 'ONE-MANY') {
r += `\t${fromCase.toLocaleLowerCase() + edgeCase}In: ${fromCase} @relationship(edgeType:"${edge.label}", direction:IN)\n`
if (direction.relationship === 'ONE-MANY') {
r += `\t${fromCase.toLocaleLowerCase() + edgeCase}In: ${fromCase} @relationship(edgeType:"${edge.label}", direction:IN)\n`;
}
if (direction.relationship == 'MANY-ONE') {
r += `\t${fromCase.toLocaleLowerCase() + edgeCase}sIn(filter: ${fromCase}Input, options: Options): [${fromCase}] @relationship(edgeType:"${edge.label}", direction:IN)\n`
if (direction.relationship === 'MANY-ONE') {
r += `\t${fromCase.toLocaleLowerCase() + edgeCase}sIn(filter: ${fromCase}Input, options: Options): [${fromCase}] @relationship(edgeType:"${edge.label}", direction:IN)\n`;
}
if (!edgeTypes.includes(edge.label))
edgeTypes.push(edge.label);
Expand All @@ -141,7 +172,7 @@ function graphDBInferenceSchema (graphbSchema, addMutations) {
? toPascalCase(edgeType)
: edgeType;

r += `\t${aliasedEdgeType}:${caseAdjustedEdgeType}`;
r += `\t${cleanseLabel(aliasedEdgeType)}:${cleanseLabel(caseAdjustedEdgeType)}\n`;
});

r += '}\n\n';
Expand All @@ -150,36 +181,59 @@ function graphDBInferenceSchema (graphbSchema, addMutations) {
r += `input ${nodeCase}Input {\n`;
r += '\t_id: ID @id\n';
node.properties.forEach(property => {
r+= `\t${property.name}: ${property.type}\n`;
let propertyCase = cleanseLabel(property.name);
if (property.name !== propertyCase) {
r+= `\t${propertyCase}: ${property.type} @alias(property: "${property.name}")\n`;
}
else {
r+= `\t${property.name}: ${property.type}\n`;
}
});
r += '}\n\n';
})

// edge types
gdbs.edgeStructures.forEach(edge => {
// edge type
let edgeCase = edge.label;
let edgeCase = cleanseLabel(edge.label);
if (changeCase) {
edgeCase = toPascalCase(edge.label);
r += `type ${edgeCase} @alias(property:"${edge.label}") {\n`;
} else {
edgeCase = toPascalCase(edgeCase);
}
if (edge.label !== edgeCase) {
r += `type ${edgeCase} @alias(property:"${edge.label}") {\n`;
}
else {
r += `type ${edgeCase} {\n`;
}
r += '\t_id: ID! @id\n';

edge.properties.forEach(property => {
if (property.name == 'id')
r+= `\tid: ID\n`;
else
r+= `\t${property.name}: ${property.type}\n`;
if (property.name === 'id') {
r += `\tid: ID\n`;
}
else {
let propertyCase = cleanseLabel(property.name);
let alias = '';
if (property.name !== propertyCase) {
alias = ` @alias(property: "${property.name}")`;
}
r+= `\t${propertyCase}: ${property.type}${alias}\n`;
}
});
r += '}\n\n';

// input for the edge type
if (edge.properties.length > 0) {
r += `input ${edgeCase}Input {\n`;
if (edge.properties.length > 0) {
r += `input ${edgeCase}Input {\n`;

edge.properties.forEach(property => {
r+= `\t${property.name}: ${property.type}\n`;
let propertyCase = cleanseLabel(property.name);
if (property.name !== propertyCase) {
r += `\t${propertyCase}: ${property.type} @alias(property: "${property.name}")\n`;
}
else {
r+= `\t${property.name}: ${property.type}\n`;
}
});
r += '}\n\n';
}
Expand All @@ -193,34 +247,35 @@ function graphDBInferenceSchema (graphbSchema, addMutations) {
// query
r += `type Query {\n`;
gdbs.nodeStructures.forEach(node => {
let nodeCase = toPascalCase(node.label);
let nodeCase = toPascalCase(cleanseLabel(node.label));
r += `\tgetNode${nodeCase}(filter: ${nodeCase}Input): ${nodeCase}\n`;
r += `\tgetNode${nodeCase}s(filter: ${nodeCase}Input, options: Options): [${nodeCase}]\n`;
});
r += '}\n\n';

// mutation
if (addMutations) {
if (addMutations) {
r += `type Mutation {\n`;
gdbs.nodeStructures.forEach(node => {
let nodeCase = toPascalCase(node.label);
let nodeCase = toPascalCase(cleanseLabel(node.label));
r += `\tcreateNode${nodeCase}(input: ${nodeCase}Input!): ${nodeCase}\n`;
r += `\tupdateNode${nodeCase}(input: ${nodeCase}Input!): ${nodeCase}\n`;
r += `\tdeleteNode${nodeCase}(_id: ID!): Boolean\n`;
});

gdbs.edgeStructures.forEach(edge => {
edge.directions.forEach(direction => {
let edgeCase = toPascalCase(edge.label);
let fromCase = toPascalCase(direction.from);
let toCase = toPascalCase(direction.to);
if (edge.properties.length > 0) {
r += `\tconnectNode${fromCase}ToNode${toCase}Edge${edgeCase}(from_id: ID!, to_id: ID!, edge: ${edgeCase}Input!): ${edgeCase}\n`;
r += `\tupdateEdge${edgeCase}From${fromCase}To${toCase}(from_id: ID!, to_id: ID!, edge: ${edgeCase}Input!): ${edgeCase}\n`;
} else {
r += `\tconnectNode${fromCase}ToNode${toCase}Edge${edgeCase}(from_id: ID!, to_id: ID!): ${edgeCase}\n`;
}
r += `\tdeleteEdge${edgeCase}From${fromCase}To${toCase}(from_id: ID!, to_id: ID!): Boolean\n`;
let fromCase = toPascalCase(cleanseLabel(direction.from));
let toCase = toPascalCase(cleanseLabel(direction.to));
let edgeCase = toPascalCase(cleanseLabel(edge.label));

if (edge.properties.length > 0) {
r += `\tconnectNode${fromCase}ToNode${toCase}Edge${edgeCase}(from_id: ID!, to_id: ID!, edge: ${edgeCase}Input!): ${edgeCase}\n`;
r += `\tupdateEdge${edgeCase}From${fromCase}To${toCase}(from_id: ID!, to_id: ID!, edge: ${edgeCase}Input!): ${edgeCase}\n`;
} else {
r += `\tconnectNode${fromCase}ToNode${toCase}Edge${edgeCase}(from_id: ID!, to_id: ID!): ${edgeCase}\n`;
}
r += `\tdeleteEdge${edgeCase}From${fromCase}To${toCase}(from_id: ID!, to_id: ID!): Boolean\n`;
});
});
r += '}\n\n';
Expand Down
Loading

0 comments on commit 2d6c08c

Please sign in to comment.