Skip to content

Commit b6c82f2

Browse files
authored
Merge pull request #38 from jchen042/fix/node-label-duplication-detect
fix: node label and object type duplication detect
2 parents fe8a8f8 + cc45630 commit b6c82f2

File tree

7 files changed

+390
-6
lines changed

7 files changed

+390
-6
lines changed

.changeset/fifty-suns-march.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@neo4j/graph-schema-utils": patch
3+
---
4+
5+
fix: detect malformed graph schema with duplicated node label ids and object type ids

.vscode/settings.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"editor.formatOnSave": true,
3+
"[javascript]": {
4+
"editor.defaultFormatter": "esbenp.prettier-vscode"
5+
},
6+
"[typescript]": {
7+
"editor.defaultFormatter": "esbenp.prettier-vscode"
8+
},
9+
"[javascriptreact]": {
10+
"editor.defaultFormatter": "esbenp.prettier-vscode"
11+
},
12+
"[typescriptreact]": {
13+
"editor.defaultFormatter": "esbenp.prettier-vscode"
14+
},
15+
"[json]": {
16+
"editor.defaultFormatter": "esbenp.prettier-vscode"
17+
},
18+
"[jsonc]": {
19+
"editor.defaultFormatter": "esbenp.prettier-vscode"
20+
}
21+
}

package-lock.json

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { describe, test } from "vitest";
2+
import { readFile } from "../../../test/fs.utils.js";
3+
import path from "path";
4+
import { fromJson } from "./index.js";
5+
import { strict as assert } from "node:assert";
6+
import {
7+
hasDuplicateNodeLabelIds,
8+
hasDuplicateNodeObjectTypeIds,
9+
} from "./extensions.js";
10+
import { RootSchemaJsonStruct } from "./types.js";
11+
12+
describe("JSON formatter", () => {
13+
describe("hasDuplicateNodeLabelIds", () => {
14+
test("Identifies duplicated node labels", () => {
15+
const schema = readFile(
16+
path.resolve(__dirname, "./test-schemas/duplicated-nodeLabel-ids.json")
17+
);
18+
const schemaJson = JSON.parse(schema) as RootSchemaJsonStruct;
19+
20+
assert.strictEqual(
21+
hasDuplicateNodeLabelIds(
22+
schemaJson.graphSchemaRepresentation.graphSchema
23+
),
24+
true
25+
);
26+
});
27+
});
28+
describe("hasDuplicateNodeObjectTypeIds", () => {
29+
test("Identifies duplicated node object types", () => {
30+
const schema = readFile(
31+
path.resolve(
32+
__dirname,
33+
"./test-schemas/duplicated-nodeObjectType-ids.json"
34+
)
35+
);
36+
const schemaJson = JSON.parse(schema) as RootSchemaJsonStruct;
37+
38+
assert.strictEqual(
39+
hasDuplicateNodeObjectTypeIds(
40+
schemaJson.graphSchemaRepresentation.graphSchema
41+
),
42+
true
43+
);
44+
});
45+
});
46+
describe("fromJson", () => {
47+
const schemaWithDuplicatedNodeLabels = readFile(
48+
path.resolve(__dirname, "./test-schemas/duplicated-nodeLabel-ids.json")
49+
);
50+
test("Identifies duplicated node labels adn throws an error", () => {
51+
assert.throws(() => fromJson(schemaWithDuplicatedNodeLabels), {
52+
message: "Duplicate node label IDs found in schema",
53+
});
54+
});
55+
56+
test("Identifies duplicated node object types and throws an error", () => {
57+
const schema = readFile(
58+
path.resolve(
59+
__dirname,
60+
"./test-schemas/duplicated-nodeObjectType-ids.json"
61+
)
62+
);
63+
assert.throws(() => fromJson(schema), {
64+
message: "Duplicate node object type IDs found in schema",
65+
});
66+
});
67+
68+
test("Does not throw an error if there are no duplicated ids in node labels or node object types", () => {
69+
const schema = readFile(
70+
path.resolve(__dirname, "./test-schemas/full.json")
71+
);
72+
assert.doesNotThrow(() => fromJson(schema));
73+
});
74+
});
75+
});

packages/graph-schema-utils/src/formatters/json/extensions.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
} from "../../model/index.js";
2424
import {
2525
ConstraintJsonStruct,
26+
GraphSchemaJsonStruct,
2627
IndexJsonStruct,
2728
isLookupIndexJsonStruct,
2829
isNodeLabelConstraintJsonStruct,
@@ -107,12 +108,48 @@ export function fromJson(schema: string): GraphSchema {
107108
return fromJsonStruct(schemaJson);
108109
}
109110

111+
export function hasDuplicateNodeLabelIds(
112+
schema: GraphSchemaJsonStruct
113+
): boolean {
114+
const ids = new Set<string>();
115+
116+
for (const nodeLabel of schema.nodeLabels) {
117+
if (ids.has(nodeLabel.$id)) {
118+
return true;
119+
}
120+
ids.add(nodeLabel.$id);
121+
}
122+
123+
return false;
124+
}
125+
126+
export function hasDuplicateNodeObjectTypeIds(
127+
schema: GraphSchemaJsonStruct
128+
): boolean {
129+
const ids = new Set<string>();
130+
131+
for (const nodeObjectType of schema.nodeObjectTypes) {
132+
if (ids.has(nodeObjectType.$id)) {
133+
return true;
134+
}
135+
ids.add(nodeObjectType.$id);
136+
}
137+
138+
return false;
139+
}
140+
110141
export function fromJsonStruct(schemaJson: RootSchemaJsonStruct): GraphSchema {
111142
const { graphSchema } = schemaJson.graphSchemaRepresentation;
143+
if (hasDuplicateNodeLabelIds(graphSchema)) {
144+
throw new Error("Duplicate node label IDs found in schema");
145+
}
112146
const labels = graphSchema.nodeLabels.map(nodeLabel.create);
113147
const relationshipTypes = graphSchema.relationshipTypes.map(
114148
relationshipType.create
115149
);
150+
if (hasDuplicateNodeObjectTypeIds(graphSchema)) {
151+
throw new Error("Duplicate node object type IDs found in schema");
152+
}
116153
const nodeObjectTypes = graphSchema.nodeObjectTypes.map(
117154
(nodeObjectTypeJson) =>
118155
nodeObjectType.create(nodeObjectTypeJson, (ref) => {
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
{
2+
"graphSchemaRepresentation": {
3+
"version": "1.0.0",
4+
"graphSchema": {
5+
"nodeLabels": [
6+
{
7+
"$id": "nl:0",
8+
"token": "DOCS_CHUNKS_TABLE",
9+
"properties": [
10+
{
11+
"$id": "p:0_0",
12+
"token": "RELATIVE_PATH",
13+
"type": {
14+
"type": "string"
15+
},
16+
"nullable": false
17+
},
18+
{
19+
"$id": "p:0_1",
20+
"token": "SIZE",
21+
"type": {
22+
"type": "integer"
23+
},
24+
"nullable": false
25+
},
26+
{
27+
"$id": "p:0_2",
28+
"token": "FILE_URL",
29+
"type": {
30+
"type": "string"
31+
},
32+
"nullable": false
33+
},
34+
{
35+
"$id": "p:0_3",
36+
"token": "SCOPED_FILE_URL",
37+
"type": {
38+
"type": "string"
39+
},
40+
"nullable": false
41+
},
42+
{
43+
"$id": "p:0_4",
44+
"token": "CHUNK",
45+
"type": {
46+
"type": "string"
47+
},
48+
"nullable": false
49+
}
50+
]
51+
},
52+
{
53+
"$id": "nl:0",
54+
"token": "DOCS_METADATA",
55+
"properties": [
56+
{
57+
"$id": "p:0_0",
58+
"token": "RELATIVE_PATH",
59+
"type": {
60+
"type": "string"
61+
},
62+
"nullable": false
63+
},
64+
{
65+
"$id": "p:0_1",
66+
"token": "MODEL_NAME",
67+
"type": {
68+
"type": "string"
69+
},
70+
"nullable": false
71+
},
72+
{
73+
"$id": "p:0_2",
74+
"token": "SKILLS",
75+
"type": {
76+
"type": "string"
77+
},
78+
"nullable": false
79+
},
80+
{
81+
"$id": "p:0_3",
82+
"token": "DOMAINS",
83+
"type": {
84+
"type": "string"
85+
},
86+
"nullable": false
87+
},
88+
{
89+
"$id": "p:0_4",
90+
"token": "SUMMARY",
91+
"type": {
92+
"type": "string"
93+
},
94+
"nullable": false
95+
}
96+
]
97+
}
98+
],
99+
"relationshipTypes": [],
100+
"nodeObjectTypes": [
101+
{
102+
"$id": "n:0",
103+
"labels": [
104+
{
105+
"$ref": "#nl:0"
106+
}
107+
]
108+
},
109+
{
110+
"$id": "n:0",
111+
"labels": [
112+
{
113+
"$ref": "#nl:0"
114+
}
115+
]
116+
}
117+
],
118+
"relationshipObjectTypes": [],
119+
"constraints": [],
120+
"indexes": []
121+
}
122+
}
123+
}

0 commit comments

Comments
 (0)