Skip to content

Commit b1a7371

Browse files
committed
Merge branch 'main' into woql/random-id-gen
2 parents 9690d6c + 1bc3284 commit b1a7371

20 files changed

+426
-22
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ jobs:
4141
run: |
4242
npm ci
4343
npm run build
44+
npm run validate-types:strict
4445
npm run test
4546
npm run test:integration
4647
- name: Lint

.husky/pre-commit

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@
44
# Lint
55
npm run lint:check;
66

7-
# Test
8-
npm test;
7+
# Check typescript
8+
npm run validate-types:strict
99

1010
# Build
1111
npm run build
1212

13+
# Test
14+
npm test;
15+

RELEASE_NOTES.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
# TerminusDB Client v11.1.2
2+
## Fixes
3+
* Adjust the release process to include version update task
4+
* Fix typescript generation and add ci tests to prevent similar errors in the future.
5+
16
# TerminusDB Client v11.1.1
27
## Fixes
38
* Update dependencies follow-redirects, webpack-dev-middleware, axios, braces, semver, micromatch, cross-spawn, word-wrap, on-headers, compression, form-data

docs/release_process.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
# Releasing a new version of the client
22

3-
1. Review the changelog between last tagged release v10.0.34..HEAD
4-
1. Create a branch and update RELEASE_NOTES.md based on changes
5-
1. Add target version number to release notes
6-
1. Check in and merge
7-
1. Pick the latest version from the RELEASE_NOTES.md file
8-
1. Update package.json version and run npm install
9-
1. Tag the repo locally and push the tag, align the release (git tag -s v11.x.x)
10-
1. The new release will be built and published 🎉
3+
1. Review the changelog between last tagged release v11.1.x..HEAD
4+
2. Pick the next version number, update package.json, and run `npm install`
5+
3. Create a branch and update RELEASE_NOTES.md based on changes
6+
4. Add target version number to release notes
7+
5. Check in and merge
8+
6. Pick the latest version from the RELEASE_NOTES.md file
9+
7. Copy the release notes in Markdown, create a tag on Github with the notes
10+
8. The new release will be built and published 🎉
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',

lib/typedef.js

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,27 @@ const { ACTIONS } = Utils.ACTIONS;
187187
/**
188188
* @typedef {Object} NamedResourceData - { filename: "data.csv", data: "col1;col2\nval1;val2" }
189189
* @property {string} filename - Filename referenced in the WOQL query
190-
* @property {string|Blob} data - Attached data, such as CSV contents
190+
* @property {string|Blob|Buffer} data - Attached data, such as CSV contents
191+
*/
192+
193+
/**
194+
* @typedef {Object} Frame - Represents a document frame, object frame, or property frame
195+
* in the viewer system. Frames are used to describe the structure and properties of data
196+
* being displayed or validated.
197+
* @property {string} [subject] - Subject identifier
198+
* @property {string} [property] - Property name
199+
* @property {string} [type] - Type information (e.g., xsd:string, schema:Person)
200+
* @property {*} [value] - Frame value
201+
* @property {number} [depth] - Depth in frame hierarchy
202+
* @property {string} [range] - Property range/type
203+
* @property {string} [label] - Display label
204+
* @property {Object} [parent] - Parent frame reference
205+
* @property {Array} [children] - Child frames
206+
* @property {string} [status] - Frame status: 'updated' | 'error' | 'new' | 'ok'
207+
* @property {boolean} [literal] - Whether this represents a literal value
208+
* @property {number} [index] - Index in parent collection
209+
* @property {Object} [frame] - Nested frame data
210+
* @property {string} [subjectClass] - Class of the subject
191211
*/
192212

193213
module.exports = {};

lib/viewer/frameRule.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88
/* eslint-disable no-use-before-define */
99
const TerminusRule = require('./terminusRule');
1010

11+
/**
12+
* @typedef {import('../typedef').Frame} Frame
13+
*/
14+
1115
/**
1216
* @file Frame Rule
1317
* @license Apache Version 2
@@ -23,7 +27,7 @@ Object.setPrototypeOf(FrameRule.prototype, TerminusRule.TerminusRule.prototype);
2327
/**
2428
* Returns an array of rules that match the paased frame
2529
* @param {[FrameRule]} rules - array of rules to be tested
26-
* @param {Frame} frame - object frame, property frame or data from to be tested
30+
* @param {Frame | object} frame - document frame, object frame, or property frame to be tested
2731
* @param {function} [onmatch] - optional function to be called with args (frame, rule)
2832
* on each match
2933
*/

lib/viewer/tableConfig.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,8 +124,10 @@ WOQLTableConfig.prototype.prettyPrint = function () {
124124
};
125125

126126
/**
127-
* @param {boolean} canfilter
128-
* @returns WOQLTableConfig
127+
* Gets or sets whether the table is filterable
128+
* @param {boolean} [canfilter] - If provided, sets the filterable state
129+
* @returns {boolean|WOQLTableConfig} - Returns the filterable state (boolean) when called
130+
* without arguments, or returns this instance (WOQLTableConfig) for chaining when setting
129131
*/
130132

131133
WOQLTableConfig.prototype.filterable = function (canfilter) {

lib/viewer/terminusRule.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ TerminusRule.prototype.literal = function (tf) {
2020
};
2121

2222
/**
23-
* @param {[TYPE_URLS]} list - parameters are types identified by prefixed URLS (xsd:string...)
23+
* @param {...string} list - parameters are types identified by prefixed URLS (xsd:string...)
2424
*/
2525
TerminusRule.prototype.type = function (...list) {
2626
if (typeof list === 'undefined' || list.length === 0) {

0 commit comments

Comments
 (0)