Skip to content

Commit bcb0988

Browse files
authored
Merge pull request #348 from hoijnet/issue/346-incorrect-json-for-nested-subdoc-in-lists
Fix update_document for lists in document templates
2 parents 9217166 + fb779a7 commit bcb0988

File tree

4 files changed

+304
-4
lines changed

4 files changed

+304
-4
lines changed

integration_tests/create_database.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,13 @@ describe('Create a database, schema and insert data', () => {
5555
test('Query Person by name', async () => {
5656
const queryTemplate = {"name":"Tom", "@type":"Person" }
5757
const result = await client.getDocument({query:queryTemplate});
58-
expect(result).toStrictEqual({ '@id': 'Child/Tom', '@type': 'Child', age: "10", name: 'Tom' });
58+
expect(result).toStrictEqual({ '@id': 'Child/Tom', '@type': 'Child', age: 10, name: 'Tom' });
5959
})
6060

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

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

7979
test('Update Child Tom, link Parent', async () => {
80-
const childTom = { '@id': 'Child/Tom', '@type': 'Child', age: "10", name: 'Tom' , has_parent:"Parent/Tom%20Senior"}
80+
const childTom = { '@id': 'Child/Tom', '@type': 'Child', age: 10, name: 'Tom' , has_parent:"Parent/Tom%20Senior"}
8181
const result = await client.updateDocument(childTom);
8282
expect(result).toStrictEqual(["terminusdb:///data/Child/Tom" ]);
8383
})
@@ -113,7 +113,7 @@ describe('Create a database, schema and insert data', () => {
113113
expect(result).toStrictEqual({
114114
'@id': 'Child/Tom',
115115
'@type': 'Child',
116-
age: "10",
116+
age: 10,
117117
name: 'Tom',
118118
has_parent: 'Parent/Tom%20Senior'
119119
});
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
//@ts-check
2+
import { describe, expect, test, beforeAll, afterAll } from '@jest/globals';
3+
import { WOQLClient, WOQL, Doc } from '../index.js';
4+
import WOQLQuery from '../lib/query/woqlQuery.js';
5+
6+
/**
7+
* Integration test for update_document with a list of subdocuments.
8+
*
9+
* This test verifies that the WOQL builder correctly handles nested Doc()
10+
* objects when updating documents containing lists of subdocuments.
11+
*
12+
* Related issue: Nested Doc() in update_document was producing incorrect
13+
* WOQL JSON structure due to double-conversion.
14+
*/
15+
16+
let client: WOQLClient;
17+
const testDbName = `update_list_test_${Date.now()}`;
18+
19+
// Schema with a subdocument list
20+
const schema = [
21+
{
22+
'@base': 'terminusdb:///data/',
23+
'@schema': 'terminusdb:///schema#',
24+
'@type': '@context',
25+
},
26+
{
27+
'@type': 'Class',
28+
'@id': 'UpdateList',
29+
'@key': { '@type': 'Random' },
30+
list: { '@class': 'Structure', '@type': 'List' },
31+
},
32+
{
33+
'@type': 'Class',
34+
'@id': 'Structure',
35+
'@key': { '@type': 'Random' },
36+
'@subdocument': [],
37+
string: 'xsd:string',
38+
},
39+
];
40+
41+
beforeAll(async () => {
42+
client = new WOQLClient('http://127.0.0.1:6363', {
43+
user: 'admin',
44+
organization: 'admin',
45+
key: process.env.TDB_ADMIN_PASS ?? 'root',
46+
});
47+
48+
// Create test database
49+
await client.createDatabase(testDbName, {
50+
label: 'Update List Test',
51+
comment: 'Test database for update_document with subdocument lists',
52+
schema: true,
53+
});
54+
client.db(testDbName);
55+
56+
// Add schema
57+
await client.addDocument(schema, { graph_type: 'schema', full_replace: true });
58+
});
59+
60+
afterAll(async () => {
61+
try {
62+
await client.deleteDatabase(testDbName);
63+
} catch (e) {
64+
// Database might not exist
65+
}
66+
});
67+
68+
describe('update_document with list of subdocuments', () => {
69+
const docId = 'UpdateList/test-doc';
70+
71+
test('should insert initial document with subdocument list', async () => {
72+
const initialDoc = {
73+
'@type': 'UpdateList',
74+
'@id': docId,
75+
list: [
76+
{ '@type': 'Structure', string: 'initial-1' },
77+
{ '@type': 'Structure', string: 'initial-2' },
78+
],
79+
};
80+
81+
const result = await client.addDocument(initialDoc);
82+
// Result contains full IRI with prefix
83+
expect(result[0]).toContain('UpdateList/test-doc');
84+
});
85+
86+
test('should update document list using WOQL.update_document with nested Doc()', async () => {
87+
// This is the pattern that was failing before the fix
88+
const query = WOQL.update_document(
89+
new (Doc as any)({
90+
'@type': 'UpdateList',
91+
'@id': docId,
92+
list: [
93+
new (Doc as any)({ '@type': 'Structure', string: 'updated-1' }),
94+
new (Doc as any)({ '@type': 'Structure', string: 'updated-2' }),
95+
new (Doc as any)({ '@type': 'Structure', string: 'updated-3' }),
96+
],
97+
}),
98+
) as WOQLQuery;
99+
100+
const result = await client.query(query);
101+
expect(result).toBeDefined();
102+
expect(result?.inserts).toBeGreaterThan(0);
103+
expect(result?.deletes).toBeGreaterThan(0);
104+
105+
// Verify the document was updated correctly
106+
const doc = await client.getDocument({ id: docId });
107+
expect(doc['@type']).toEqual('UpdateList');
108+
expect(doc.list).toHaveLength(3);
109+
expect(doc.list[0].string).toEqual('updated-1');
110+
expect(doc.list[1].string).toEqual('updated-2');
111+
expect(doc.list[2].string).toEqual('updated-3');
112+
});
113+
114+
test('should update document list using plain objects (alternative syntax)', async () => {
115+
// Alternative approach without nested Doc() - should also work
116+
const query = WOQL.update_document(
117+
new (Doc as any)({
118+
'@type': 'UpdateList',
119+
'@id': docId,
120+
list: [
121+
{ '@type': 'Structure', string: 'plain-1' },
122+
{ '@type': 'Structure', string: 'plain-2' },
123+
],
124+
}),
125+
) as WOQLQuery;
126+
127+
const result = await client.query(query);
128+
expect(result).toBeDefined();
129+
130+
// Verify the document was updated correctly
131+
const doc = await client.getDocument({ id: docId });
132+
expect(doc.list).toHaveLength(2);
133+
expect(doc.list[0].string).toEqual('plain-1');
134+
expect(doc.list[1].string).toEqual('plain-2');
135+
});
136+
137+
test('should update to empty list', async () => {
138+
const query = WOQL.update_document(
139+
new (Doc as any)({
140+
'@type': 'UpdateList',
141+
'@id': docId,
142+
list: [],
143+
}),
144+
) as WOQLQuery;
145+
146+
const result = await client.query(query);
147+
expect(result).toBeDefined();
148+
149+
// Verify the list is now empty
150+
const doc = await client.getDocument({ id: docId });
151+
expect(doc.list).toEqual([]);
152+
});
153+
});

lib/query/woqlDoc.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,32 @@
1+
/**
2+
* Check if an object is already a converted WOQL Value structure
3+
* @param {object} obj
4+
* @returns {boolean}
5+
*/
6+
function isAlreadyConverted(obj) {
7+
if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {
8+
return false;
9+
}
10+
// A converted Value has @type: 'Value' and one of: dictionary, list, data, node, variable
11+
if (obj['@type'] === 'Value') {
12+
return (
13+
obj.dictionary !== undefined
14+
|| obj.list !== undefined
15+
|| obj.data !== undefined
16+
|| obj.node !== undefined
17+
|| obj.variable !== undefined
18+
);
19+
}
20+
return false;
21+
}
22+
123
// eslint-disable-next-line consistent-return
224
function convert(obj) {
325
if (obj == null) {
426
return null;
27+
} if (isAlreadyConverted(obj)) {
28+
// Object is already a converted WOQL Value structure, return as-is
29+
return obj;
530
} if (typeof (obj) === 'number') {
631
return {
732
'@type': 'Value',

test/woqlUpdateDocWithList.spec.js

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
const { expect } = require('chai');
2+
3+
const WOQL = require('../lib/woql');
4+
const { Doc } = require('../lib/query/woqlDoc');
5+
6+
/**
7+
* Test for update_document with a list of subdocuments
8+
*
9+
* This test verifies that the WOQL builder correctly converts
10+
* update_document calls with nested Doc() objects containing lists
11+
* of subdocuments.
12+
*
13+
* Issue: When using new Doc() inside another Doc(), the convert()
14+
* function may double-wrap the already-converted structure.
15+
*/
16+
describe('WOQL update_document with list of subdocuments', () => {
17+
// The expected correct WOQL JSON structure for updating a document
18+
// with a list of subdocuments
19+
const expectedUpdateDocWithListJson = {
20+
'@type': 'UpdateDocument',
21+
document: {
22+
'@type': 'Value',
23+
dictionary: {
24+
'@type': 'DictionaryTemplate',
25+
data: [
26+
{
27+
'@type': 'FieldValuePair',
28+
field: '@type',
29+
value: { '@type': 'Value', data: { '@type': 'xsd:string', '@value': 'UpdateList' } },
30+
},
31+
{
32+
'@type': 'FieldValuePair',
33+
field: '@id',
34+
value: { '@type': 'Value', data: { '@type': 'xsd:string', '@value': 'UpdateList/list' } },
35+
},
36+
{
37+
'@type': 'FieldValuePair',
38+
field: 'list',
39+
value: {
40+
'@type': 'Value',
41+
list: [
42+
{
43+
'@type': 'Value',
44+
dictionary: {
45+
'@type': 'DictionaryTemplate',
46+
data: [
47+
{
48+
'@type': 'FieldValuePair',
49+
field: '@type',
50+
value: { '@type': 'Value', data: { '@type': 'xsd:string', '@value': 'Structure' } },
51+
},
52+
{
53+
'@type': 'FieldValuePair',
54+
field: 'string',
55+
value: { '@type': 'Value', data: { '@type': 'xsd:string', '@value': '3' } },
56+
},
57+
],
58+
},
59+
},
60+
],
61+
},
62+
},
63+
],
64+
},
65+
},
66+
};
67+
68+
it('should correctly convert update_document with nested Doc in list', () => {
69+
// This is what the user is trying to do:
70+
const woqlObject = WOQL.update_document(
71+
new Doc({
72+
'@type': 'UpdateList',
73+
'@id': 'UpdateList/list',
74+
list: [new Doc({ '@type': 'Structure', string: '3' })],
75+
}),
76+
);
77+
78+
const result = woqlObject.json();
79+
expect(result).to.deep.equal(expectedUpdateDocWithListJson);
80+
});
81+
82+
it('should correctly convert Doc with list of plain objects (no nested Doc)', () => {
83+
// Alternative approach: use plain objects in the list
84+
const woqlObject = WOQL.update_document(
85+
new Doc({
86+
'@type': 'UpdateList',
87+
'@id': 'UpdateList/list',
88+
list: [{ '@type': 'Structure', string: '3' }], // Plain object, not new Doc()
89+
}),
90+
);
91+
92+
const result = woqlObject.json();
93+
expect(result).to.deep.equal(expectedUpdateDocWithListJson);
94+
});
95+
96+
it('should show what new Doc() returns directly', () => {
97+
// Test what Doc returns to understand the conversion
98+
const subdoc = new Doc({ '@type': 'Structure', string: '3' });
99+
100+
// Doc should return a properly converted Value/DictionaryTemplate structure
101+
const expectedSubdoc = {
102+
'@type': 'Value',
103+
dictionary: {
104+
'@type': 'DictionaryTemplate',
105+
data: [
106+
{
107+
'@type': 'FieldValuePair',
108+
field: '@type',
109+
value: { '@type': 'Value', data: { '@type': 'xsd:string', '@value': 'Structure' } },
110+
},
111+
{
112+
'@type': 'FieldValuePair',
113+
field: 'string',
114+
value: { '@type': 'Value', data: { '@type': 'xsd:string', '@value': '3' } },
115+
},
116+
],
117+
},
118+
};
119+
120+
expect(subdoc).to.deep.equal(expectedSubdoc);
121+
});
122+
});

0 commit comments

Comments
 (0)