Skip to content

Commit ccc7374

Browse files
authored
Merge pull request #347 from hoijnet/woql/random-id-gen
Adding random id generation to WOQL JS
2 parents 1aaee84 + c285711 commit ccc7374

File tree

5 files changed

+260
-0
lines changed

5 files changed

+260
-0
lines changed
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
//@ts-check
2+
import { describe, expect, test, beforeAll, afterAll } from "@jest/globals";
3+
import { WOQLClient, WOQL } from "../index.js";
4+
import { DbDetails } from "../dist/typescript/lib/typedef.js";
5+
6+
let client: WOQLClient;
7+
8+
beforeAll(() => {
9+
client = new WOQLClient("http://127.0.0.1:6363", {
10+
user: "admin",
11+
organization: "admin",
12+
key: process.env.TDB_ADMIN_PASS ?? "root"
13+
});
14+
});
15+
16+
const testDb = "db__test_woql_random_idgen";
17+
18+
describe("WOQL Random ID Generation", () => {
19+
test("Setup: Create database and schema", async () => {
20+
const dbObj: DbDetails = {
21+
label: testDb,
22+
comment: "Test database for random ID generation",
23+
schema: true
24+
};
25+
const result = await client.createDatabase(testDb, dbObj);
26+
expect(result["@type"]).toEqual("api:DbCreateResponse");
27+
28+
const schema = [
29+
{
30+
"@type": "Class",
31+
"@id": "Person",
32+
"@key": { "@type": "Random" },
33+
name: "xsd:string"
34+
}
35+
];
36+
37+
await client.addDocument(schema, { graph_type: "schema" });
38+
});
39+
40+
test("Generate random ID using WOQL", async () => {
41+
const query = WOQL.random_idgen("Person/", "v:person_id");
42+
43+
const result = await client.query(query);
44+
expect(result?.bindings).toBeDefined();
45+
expect(result?.bindings?.length).toBeGreaterThan(0);
46+
47+
// Server returns bindings without the 'v:' prefix
48+
const binding = result?.bindings?.[0];
49+
const personId = binding["person_id"] || binding["v:person_id"];
50+
expect(personId).toBeDefined();
51+
expect(personId).toContain("Person/");
52+
53+
// Should have a 16-character random suffix
54+
const suffix = personId.split("Person/")[1];
55+
expect(suffix.length).toBe(16);
56+
});
57+
58+
test("Generate multiple unique IDs", async () => {
59+
const query = WOQL.and(
60+
WOQL.random_idgen("Person/", "v:id1"),
61+
WOQL.random_idgen("Person/", "v:id2"),
62+
WOQL.random_idgen("Person/", "v:id3")
63+
);
64+
65+
const result = await client.query(query);
66+
expect(result?.bindings).toBeDefined();
67+
expect(result?.bindings?.length).toBe(1);
68+
69+
const binding = result?.bindings?.[0];
70+
const id1 = binding["id1"] || binding["v:id1"];
71+
const id2 = binding["id2"] || binding["v:id2"];
72+
const id3 = binding["id3"] || binding["v:id3"];
73+
74+
// All IDs should be different
75+
expect(id1).not.toEqual(id2);
76+
expect(id1).not.toEqual(id3);
77+
expect(id2).not.toEqual(id3);
78+
79+
// All should start with prefix
80+
expect(id1).toContain("Person/");
81+
expect(id2).toContain("Person/");
82+
expect(id3).toContain("Person/");
83+
});
84+
85+
test("Use random ID to create document", async () => {
86+
//Generate random ID
87+
const query = WOQL.random_idgen("Person/", "v:new_person");
88+
89+
const result = await client.query(query);
90+
expect(result?.bindings).toBeDefined();
91+
92+
const binding = result?.bindings?.[0];
93+
const personId = binding["new_person"] || binding["v:new_person"];
94+
expect(personId).toBeDefined();
95+
expect(personId).toContain("Person/");
96+
97+
// Create the document using the generated ID
98+
const doc = {
99+
"@type": "Person",
100+
"@id": personId,
101+
name: "Alice"
102+
};
103+
104+
await client.addDocument(doc);
105+
106+
// Verify the document was created
107+
const retrieved: any = await client.getDocument({ id: personId });
108+
expect(retrieved["@id"]).toEqual(personId);
109+
expect(retrieved.name).toEqual("Alice");
110+
});
111+
112+
test("Generate different IDs on repeated query execution", async () => {
113+
const query = WOQL.random_idgen("Data/", "v:id");
114+
const ids = new Set<string>();
115+
116+
for (let i = 0; i < 10; i++) {
117+
const result = await client.query(query);
118+
const binding = result?.bindings?.[0];
119+
const id = binding["id"] || binding["v:id"];
120+
ids.add(id);
121+
}
122+
123+
// All 10 executions should produce unique IDs
124+
expect(ids.size).toBe(10);
125+
});
126+
127+
test("Mix WOQL builder with raw JSON-LD", async () => {
128+
// Test mixing WOQL builder syntax with raw JSON-LD
129+
// This verifies the pattern documented in woql-json-ld-queries guide
130+
const query1 = WOQL.and(
131+
WOQL.random_idgen("Test/", "v:test_id"),
132+
{
133+
"@type": "LexicalKey",
134+
base: {
135+
"@type": "DataValue",
136+
data: {
137+
"@type": "xsd:string",
138+
"@value": "Display/"
139+
}
140+
},
141+
key_list: [],
142+
uri: {
143+
"@type": "NodeValue",
144+
variable: "out"
145+
}
146+
} as any
147+
);
148+
149+
const result1 = await client.query(query1);
150+
const binding1 = result1?.bindings?.[0];
151+
152+
// Verify random_idgen result
153+
const id1 = binding1["test_id"] || binding1["v:test_id"];
154+
expect(id1).toBeDefined();
155+
expect(id1).toContain("Test/");
156+
157+
// Verify the ID has correct 16-character suffix
158+
const suffix = id1.split("Test/")[1];
159+
expect(suffix.length).toBe(16);
160+
161+
// Verify LexicalKey result (empty key_list generates just the base)
162+
const out = binding1["out"] || binding1["v:out"];
163+
expect(out).toBeDefined();
164+
expect(out).toContain("Display/");
165+
});
166+
167+
test("Both random_idgen and idgen_random aliases work", async () => {
168+
// Test random_idgen alias
169+
const query1 = WOQL.random_idgen("Test/", "v:test_id");
170+
const result1 = await client.query(query1);
171+
const binding1 = result1?.bindings?.[0];
172+
const id1 = binding1["test_id"] || binding1["v:test_id"];
173+
expect(id1).toBeDefined();
174+
expect(id1).toContain("Test/");
175+
176+
// Test idgen_random alias (should produce same structure)
177+
const query2 = WOQL.idgen_random("Test/", "v:test_id");
178+
const result2 = await client.query(query2);
179+
const binding2 = result2?.bindings?.[0];
180+
const id2 = binding2["test_id"] || binding2["v:test_id"];
181+
expect(id2).toBeDefined();
182+
expect(id2).toContain("Test/");
183+
184+
// Both queries should produce the same JSON structure (same variable name)
185+
expect(query1.json()).toEqual(query2.json());
186+
});
187+
188+
afterAll(async () => {
189+
try {
190+
await client.deleteDatabase(testDb);
191+
} catch (e) {
192+
// Ignore errors
193+
}
194+
});
195+
});

lib/query/woqlQuery.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1175,6 +1175,29 @@ WOQLQuery.prototype.idgen = function (prefix, inputVarList, outputVar) {
11751175

11761176
WOQLQuery.prototype.idgenerator = WOQLQuery.prototype.idgen;
11771177

1178+
/**
1179+
* Generates a random ID with a specified prefix
1180+
* Uses cryptographically secure random base64 encoding to generate unique identifiers
1181+
* @param {string} prefix - prefix for the generated ID
1182+
* @param {string} outputVar - variable that stores the generated ID
1183+
* @returns {WOQLQuery} A WOQLQuery which contains the random ID generation pattern
1184+
* @example
1185+
* idgen_random("Person/", "v:person_id")
1186+
*/
1187+
WOQLQuery.prototype.idgen_random = function (prefix, outputVar) {
1188+
if (this.cursor['@type']) this.wrapCursorWithAnd();
1189+
this.cursor['@type'] = 'RandomKey';
1190+
this.cursor.base = this.cleanDataValue(prefix, 'xsd:string');
1191+
this.cursor.uri = this.cleanNodeValue(outputVar);
1192+
return this;
1193+
};
1194+
1195+
/**
1196+
* Backward-compatible alias for idgen_random
1197+
* @deprecated Use idgen_random instead
1198+
*/
1199+
WOQLQuery.prototype.random_idgen = WOQLQuery.prototype.idgen_random;
1200+
11781201
/**
11791202
* Changes a string to upper-case
11801203
* @param {string|Var} inputVarName - string or variable representing the uncapitalized string

lib/woql.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -752,6 +752,30 @@ WOQL.idgenerator = function (prefix, inputVarList, resultVarName) {
752752
return new WOQLQuery().idgen(prefix, inputVarList, resultVarName);
753753
};
754754

755+
/**
756+
* Generates a random ID with a specified prefix
757+
* @param {string} prefix - prefix for the generated ID
758+
* @param {string} resultVarName - variable that stores the generated ID
759+
* @returns {WOQLQuery} A WOQLQuery object containing the random ID generation function
760+
* @example
761+
* let [newid] = vars("newid")
762+
* idgen_random("Person/", newid)
763+
*/
764+
WOQL.idgen_random = function (prefix, resultVarName) {
765+
return new WOQLQuery().idgen_random(prefix, resultVarName);
766+
};
767+
768+
/**
769+
* Backward-compatible alias for idgen_random
770+
* @deprecated Use idgen_random instead
771+
* @param {string} prefix - prefix for the generated ID
772+
* @param {string} resultVarName - variable that stores the generated ID
773+
* @returns {WOQLQuery} A WOQLQuery object containing the random ID generation function
774+
*/
775+
WOQL.random_idgen = function (prefix, resultVarName) {
776+
return new WOQLQuery().idgen_random(prefix, resultVarName);
777+
};
778+
755779
/**
756780
*
757781
* Changes a string to upper-case

test/woql.spec.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const WOQL = require('../lib/woql');
44
const { Var, VarUnique, Vars } = require('../lib/query/woqlDoc');
55

66
const idGenJson = require('./woqlJson/woqlIdgenJson');
7+
const randomKeyJson = require('./woqlJson/woqlRandomKeyJson');
78
const woqlStarJson = require('./woqlJson/woqlStarJson');
89
const woqlInsertJson = require('./woqlJson/woqlInsertJson');
910
const woqlDoctypeJson = require('./woqlJson/woqlDoctypeJson');
@@ -285,6 +286,12 @@ describe('woql queries', () => {
285286
expect(woqlObject.json()).to.eql(idGenJson);
286287
});
287288

289+
it('check the idgen_random method', () => {
290+
const woqlObject = WOQL.idgen_random('Person/', 'v:Person_ID');
291+
292+
expect(woqlObject.json()).to.eql(randomKeyJson);
293+
});
294+
288295
it('check the typecast method', () => {
289296
const woqlObject = WOQL.typecast('v:Duration', 'xsd:integer', 'v:Duration_Cast');
290297

test/woqlJson/woqlRandomKeyJson.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
module.exports = {
2+
'@type': 'RandomKey',
3+
base: {
4+
'@type': 'DataValue',
5+
data: { '@type': 'xsd:string', '@value': 'Person/' },
6+
},
7+
uri: {
8+
'@type': 'NodeValue',
9+
variable: 'Person_ID',
10+
},
11+
};

0 commit comments

Comments
 (0)