Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions integration_tests/create_database.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,13 @@ describe('Create a database, schema and insert data', () => {
test('Query Person by name', async () => {
const queryTemplate = {"name":"Tom", "@type":"Person" }
const result = await client.getDocument({query:queryTemplate});
expect(result).toStrictEqual({ '@id': 'Child/Tom', '@type': 'Child', age: "10", name: 'Tom' });
expect(result).toStrictEqual({ '@id': 'Child/Tom', '@type': 'Child', age: 10, name: 'Tom' });
})

test('Query Person by ege', async () => {
const queryTemplate = {"age":"40", "@type":"Person" }
const result = await client.getDocument({query:queryTemplate});
expect(result).toStrictEqual({"@id": "Parent/Tom%20Senior", "age":"40","name":"Tom Senior","@type":"Parent" , "has_child":"Child/Tom"});
expect(result).toStrictEqual({"@id": "Parent/Tom%20Senior", "age":40,"name":"Tom Senior","@type":"Parent" , "has_child":"Child/Tom"});
})

const change_request = "change_request02";
Expand All @@ -77,7 +77,7 @@ describe('Create a database, schema and insert data', () => {
})

test('Update Child Tom, link Parent', async () => {
const childTom = { '@id': 'Child/Tom', '@type': 'Child', age: "10", name: 'Tom' , has_parent:"Parent/Tom%20Senior"}
const childTom = { '@id': 'Child/Tom', '@type': 'Child', age: 10, name: 'Tom' , has_parent:"Parent/Tom%20Senior"}
const result = await client.updateDocument(childTom);
expect(result).toStrictEqual(["terminusdb:///data/Child/Tom" ]);
})
Expand Down Expand Up @@ -113,7 +113,7 @@ describe('Create a database, schema and insert data', () => {
expect(result).toStrictEqual({
'@id': 'Child/Tom',
'@type': 'Child',
age: "10",
age: 10,
name: 'Tom',
has_parent: 'Parent/Tom%20Senior'
});
Expand Down
195 changes: 195 additions & 0 deletions integration_tests/woql_random_idgen.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
//@ts-check
import { describe, expect, test, beforeAll, afterAll } from "@jest/globals";
import { WOQLClient, WOQL } from "../index.js";
import { DbDetails } from "../dist/typescript/lib/typedef.js";

let client: WOQLClient;

beforeAll(() => {
client = new WOQLClient("http://127.0.0.1:6363", {
user: "admin",
organization: "admin",
key: process.env.TDB_ADMIN_PASS ?? "root"
});
});

const testDb = "db__test_woql_random_idgen";

describe("WOQL Random ID Generation", () => {
test("Setup: Create database and schema", async () => {
const dbObj: DbDetails = {
label: testDb,
comment: "Test database for random ID generation",
schema: true
};
const result = await client.createDatabase(testDb, dbObj);
expect(result["@type"]).toEqual("api:DbCreateResponse");

const schema = [
{
"@type": "Class",
"@id": "Person",
"@key": { "@type": "Random" },
name: "xsd:string"
}
];

await client.addDocument(schema, { graph_type: "schema" });
});

test("Generate random ID using WOQL", async () => {
const query = WOQL.random_idgen("Person/", "v:person_id");

const result = await client.query(query);
expect(result?.bindings).toBeDefined();
expect(result?.bindings?.length).toBeGreaterThan(0);

// Server returns bindings without the 'v:' prefix
const binding = result?.bindings?.[0];
const personId = binding["person_id"] || binding["v:person_id"];
expect(personId).toBeDefined();
expect(personId).toContain("Person/");

// Should have a 16-character random suffix
const suffix = personId.split("Person/")[1];
expect(suffix.length).toBe(16);
});

test("Generate multiple unique IDs", async () => {
const query = WOQL.and(
WOQL.random_idgen("Person/", "v:id1"),
WOQL.random_idgen("Person/", "v:id2"),
WOQL.random_idgen("Person/", "v:id3")
);

const result = await client.query(query);
expect(result?.bindings).toBeDefined();
expect(result?.bindings?.length).toBe(1);

const binding = result?.bindings?.[0];
const id1 = binding["id1"] || binding["v:id1"];
const id2 = binding["id2"] || binding["v:id2"];
const id3 = binding["id3"] || binding["v:id3"];

// All IDs should be different
expect(id1).not.toEqual(id2);
expect(id1).not.toEqual(id3);
expect(id2).not.toEqual(id3);

// All should start with prefix
expect(id1).toContain("Person/");
expect(id2).toContain("Person/");
expect(id3).toContain("Person/");
});

test("Use random ID to create document", async () => {
//Generate random ID
const query = WOQL.random_idgen("Person/", "v:new_person");

const result = await client.query(query);
expect(result?.bindings).toBeDefined();

const binding = result?.bindings?.[0];
const personId = binding["new_person"] || binding["v:new_person"];
expect(personId).toBeDefined();
expect(personId).toContain("Person/");

// Create the document using the generated ID
const doc = {
"@type": "Person",
"@id": personId,
name: "Alice"
};

await client.addDocument(doc);

// Verify the document was created
const retrieved: any = await client.getDocument({ id: personId });
expect(retrieved["@id"]).toEqual(personId);
expect(retrieved.name).toEqual("Alice");
});

test("Generate different IDs on repeated query execution", async () => {
const query = WOQL.random_idgen("Data/", "v:id");
const ids = new Set<string>();

for (let i = 0; i < 10; i++) {
const result = await client.query(query);
const binding = result?.bindings?.[0];
const id = binding["id"] || binding["v:id"];
ids.add(id);
}

// All 10 executions should produce unique IDs
expect(ids.size).toBe(10);
});

test("Mix WOQL builder with raw JSON-LD", async () => {
// Test mixing WOQL builder syntax with raw JSON-LD
// This verifies the pattern documented in woql-json-ld-queries guide
const query1 = WOQL.and(
WOQL.random_idgen("Test/", "v:test_id"),
{
"@type": "LexicalKey",
base: {
"@type": "DataValue",
data: {
"@type": "xsd:string",
"@value": "Display/"
}
},
key_list: [],
uri: {
"@type": "NodeValue",
variable: "out"
}
} as any
);

const result1 = await client.query(query1);
const binding1 = result1?.bindings?.[0];

// Verify random_idgen result
const id1 = binding1["test_id"] || binding1["v:test_id"];
expect(id1).toBeDefined();
expect(id1).toContain("Test/");

// Verify the ID has correct 16-character suffix
const suffix = id1.split("Test/")[1];
expect(suffix.length).toBe(16);

// Verify LexicalKey result (empty key_list generates just the base)
const out = binding1["out"] || binding1["v:out"];
expect(out).toBeDefined();
expect(out).toContain("Display/");
});

test("Both random_idgen and idgen_random aliases work", async () => {
// Test random_idgen alias
const query1 = WOQL.random_idgen("Test/", "v:test_id");
const result1 = await client.query(query1);
const binding1 = result1?.bindings?.[0];
const id1 = binding1["test_id"] || binding1["v:test_id"];
expect(id1).toBeDefined();
expect(id1).toContain("Test/");

// Test idgen_random alias (should produce same structure)
const query2 = WOQL.idgen_random("Test/", "v:test_id");
const result2 = await client.query(query2);
const binding2 = result2?.bindings?.[0];
const id2 = binding2["test_id"] || binding2["v:test_id"];
expect(id2).toBeDefined();
expect(id2).toContain("Test/");

// Both queries should produce the same JSON structure (same variable name)
expect(query1.json()).toEqual(query2.json());
});

afterAll(async () => {
try {
await client.deleteDatabase(testDb);
} catch (e) {
// Ignore errors
}
});
});
153 changes: 153 additions & 0 deletions integration_tests/woql_update_document_list.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
//@ts-check
import { describe, expect, test, beforeAll, afterAll } from '@jest/globals';
import { WOQLClient, WOQL, Doc } from '../index.js';
import WOQLQuery from '../lib/query/woqlQuery.js';

/**
* Integration test for update_document with a list of subdocuments.
*
* This test verifies that the WOQL builder correctly handles nested Doc()
* objects when updating documents containing lists of subdocuments.
*
* Related issue: Nested Doc() in update_document was producing incorrect
* WOQL JSON structure due to double-conversion.
*/

let client: WOQLClient;
const testDbName = `update_list_test_${Date.now()}`;

// Schema with a subdocument list
const schema = [
{
'@base': 'terminusdb:///data/',
'@schema': 'terminusdb:///schema#',
'@type': '@context',
},
{
'@type': 'Class',
'@id': 'UpdateList',
'@key': { '@type': 'Random' },
list: { '@class': 'Structure', '@type': 'List' },
},
{
'@type': 'Class',
'@id': 'Structure',
'@key': { '@type': 'Random' },
'@subdocument': [],
string: 'xsd:string',
},
];

beforeAll(async () => {
client = new WOQLClient('http://127.0.0.1:6363', {
user: 'admin',
organization: 'admin',
key: process.env.TDB_ADMIN_PASS ?? 'root',
});

// Create test database
await client.createDatabase(testDbName, {
label: 'Update List Test',
comment: 'Test database for update_document with subdocument lists',
schema: true,
});
client.db(testDbName);

// Add schema
await client.addDocument(schema, { graph_type: 'schema', full_replace: true });
});

afterAll(async () => {
try {
await client.deleteDatabase(testDbName);
} catch (e) {
// Database might not exist
}
});

describe('update_document with list of subdocuments', () => {
const docId = 'UpdateList/test-doc';

test('should insert initial document with subdocument list', async () => {
const initialDoc = {
'@type': 'UpdateList',
'@id': docId,
list: [
{ '@type': 'Structure', string: 'initial-1' },
{ '@type': 'Structure', string: 'initial-2' },
],
};

const result = await client.addDocument(initialDoc);
// Result contains full IRI with prefix
expect(result[0]).toContain('UpdateList/test-doc');
});

test('should update document list using WOQL.update_document with nested Doc()', async () => {
// This is the pattern that was failing before the fix
const query = WOQL.update_document(
new (Doc as any)({
'@type': 'UpdateList',
'@id': docId,
list: [
new (Doc as any)({ '@type': 'Structure', string: 'updated-1' }),
new (Doc as any)({ '@type': 'Structure', string: 'updated-2' }),
new (Doc as any)({ '@type': 'Structure', string: 'updated-3' }),
],
}),
) as WOQLQuery;

const result = await client.query(query);
expect(result).toBeDefined();
expect(result?.inserts).toBeGreaterThan(0);
expect(result?.deletes).toBeGreaterThan(0);

// Verify the document was updated correctly
const doc = await client.getDocument({ id: docId });
expect(doc['@type']).toEqual('UpdateList');
expect(doc.list).toHaveLength(3);
expect(doc.list[0].string).toEqual('updated-1');
expect(doc.list[1].string).toEqual('updated-2');
expect(doc.list[2].string).toEqual('updated-3');
});

test('should update document list using plain objects (alternative syntax)', async () => {
// Alternative approach without nested Doc() - should also work
const query = WOQL.update_document(
new (Doc as any)({
'@type': 'UpdateList',
'@id': docId,
list: [
{ '@type': 'Structure', string: 'plain-1' },
{ '@type': 'Structure', string: 'plain-2' },
],
}),
) as WOQLQuery;

const result = await client.query(query);
expect(result).toBeDefined();

// Verify the document was updated correctly
const doc = await client.getDocument({ id: docId });
expect(doc.list).toHaveLength(2);
expect(doc.list[0].string).toEqual('plain-1');
expect(doc.list[1].string).toEqual('plain-2');
});

test('should update to empty list', async () => {
const query = WOQL.update_document(
new (Doc as any)({
'@type': 'UpdateList',
'@id': docId,
list: [],
}),
) as WOQLQuery;

const result = await client.query(query);
expect(result).toBeDefined();

// Verify the list is now empty
const doc = await client.getDocument({ id: docId });
expect(doc.list).toEqual([]);
});
});
Loading