qvdjs 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +733 -0
- package/dist/index.cjs +1615 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.js +1607 -0
- package/dist/index.js.map +1 -0
- package/package.json +71 -0
package/README.md
ADDED
|
@@ -0,0 +1,733 @@
|
|
|
1
|
+
# qvdjs
|
|
2
|
+
|
|
3
|
+
> Utility library for reading/writing Qlik View Data (QVD) files in JavaScript.
|
|
4
|
+
|
|
5
|
+
## ⚠️ Important Disclaimer
|
|
6
|
+
|
|
7
|
+
**This library is based on reverse engineering of the QVD file format**, as the format is not publicly documented by Qlik. While extensive effort has been made to understand and implement the format correctly, there may be incorrect assumptions or interpretations of the file structure.
|
|
8
|
+
|
|
9
|
+
**Comprehensive testing has been performed** to ensure that QVD files created or modified by qvdjs are valid and can be loaded by Qlik Sense and QlikView without errors. However, users should:
|
|
10
|
+
|
|
11
|
+
- Test thoroughly with their specific use cases
|
|
12
|
+
- Validate output files in their Qlik environment
|
|
13
|
+
- Report any issues or inconsistencies discovered
|
|
14
|
+
|
|
15
|
+
The library works with real-world QVD files and maintains compatibility with Qlik products, but it is an independent, community-driven implementation.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
The _qvdjs_ library provides a simple API for reading/writing Qlik View Data (QVD) files in JavaScript.
|
|
20
|
+
|
|
21
|
+
Using this library, it is possible to parse the binary QVD file format and convert it to a JavaScript object
|
|
22
|
+
structure and vice versa. The library is written to be used in a Node.js environment exclusively.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
- [qvdjs](#qvdjs)
|
|
27
|
+
- [⚠️ Important Disclaimer](#️-important-disclaimer)
|
|
28
|
+
- [Install](#install)
|
|
29
|
+
- [Usage](#usage)
|
|
30
|
+
- [Lazy Loading](#lazy-loading)
|
|
31
|
+
- [Progress Tracking for Large File Writes](#progress-tracking-for-large-file-writes)
|
|
32
|
+
- [Working with Metadata](#working-with-metadata)
|
|
33
|
+
- [Security Considerations](#security-considerations)
|
|
34
|
+
- [QVD File Format](#qvd-file-format)
|
|
35
|
+
- [XML Header](#xml-header)
|
|
36
|
+
- [Symbol Table](#symbol-table)
|
|
37
|
+
- [Index Table](#index-table)
|
|
38
|
+
- [Empty QVD Files](#empty-qvd-files)
|
|
39
|
+
- [API Documentation](#api-documentation)
|
|
40
|
+
- [QvdDataFrame](#qvddataframe)
|
|
41
|
+
- [`static fromQvd(path: string, options?: object): Promise<QvdDataFrame>`](#static-fromqvdpath-string-options-object-promiseqvddataframe)
|
|
42
|
+
- [`static fromDict(dict: object): Promise<QvdDataFrame>`](#static-fromdictdict-object-promiseqvddataframe)
|
|
43
|
+
- [`head(n: number): QvdDataFrame`](#headn-number-qvddataframe)
|
|
44
|
+
- [`tail(n: number): QvdDataFrame`](#tailn-number-qvddataframe)
|
|
45
|
+
- [`rows(...args: number): QvdDataFrame`](#rowsargs-number-qvddataframe)
|
|
46
|
+
- [`at(row: number, column: string): any`](#atrow-number-column-string-any)
|
|
47
|
+
- [`select(...args: string): QvdDataFrame`](#selectargs-string-qvddataframe)
|
|
48
|
+
- [`toDict(): Promise<object>`](#todict-promiseobject)
|
|
49
|
+
- [`toQvd(path: string, options?: object): Promise<void>`](#toqvdpath-string-options-object-promisevoid)
|
|
50
|
+
- [`getFieldMetadata(fieldName: string): object | null`](#getfieldmetadatafieldname-string-object--null)
|
|
51
|
+
- [`getAllFieldMetadata(): object[]`](#getallfieldmetadata-object)
|
|
52
|
+
- [`setFileMetadata(metadata: object): void`](#setfilemetadatametadata-object-void)
|
|
53
|
+
- [`setFieldMetadata(fieldName: string, metadata: object): void`](#setfieldmetadatafieldname-string-metadata-object-void)
|
|
54
|
+
- [Documentation](#documentation)
|
|
55
|
+
- [For Users](#for-users)
|
|
56
|
+
- [For Contributors](#for-contributors)
|
|
57
|
+
- [Quick Links by Task](#quick-links-by-task)
|
|
58
|
+
- [Testing](#testing)
|
|
59
|
+
- [Running Tests](#running-tests)
|
|
60
|
+
- [Contributing](#contributing)
|
|
61
|
+
- [Contributors](#contributors)
|
|
62
|
+
- [License](#license)
|
|
63
|
+
- [Forbidden](#forbidden)
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Install
|
|
68
|
+
|
|
69
|
+
_qvdjs_ is a Node.js module available through [npm](https://www.npmjs.com/). The recommended way to install and maintain _qvdjs_ as a dependency is through the Node.js Package Manager (NPM).
|
|
70
|
+
Before installing this library, download and install Node.js.
|
|
71
|
+
|
|
72
|
+
You can get _qvdjs_ using the following command:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
npm install qvdjs --save
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**Module Format Support:**
|
|
79
|
+
This library is published as a **dual ESM/CJS package**, providing full compatibility with both modern ES modules and traditional CommonJS environments:
|
|
80
|
+
|
|
81
|
+
- ✅ **ESM (ES Modules)**: Native `import` statements in Node.js and modern bundlers
|
|
82
|
+
- ✅ **CommonJS**: Traditional `require()` for compatibility with older Node.js projects
|
|
83
|
+
- ✅ **Bundlers**: Works with Webpack, Vite, Rollup, esbuild, and other modern build tools
|
|
84
|
+
|
|
85
|
+
**Usage Examples:**
|
|
86
|
+
|
|
87
|
+
```javascript
|
|
88
|
+
// ESM (ES Modules) - Modern Node.js and TypeScript
|
|
89
|
+
import {QvdDataFrame} from 'qvdjs';
|
|
90
|
+
|
|
91
|
+
// CommonJS - Traditional Node.js
|
|
92
|
+
const {QvdDataFrame} = require('qvdjs');
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The package automatically provides the correct format based on your project's configuration.
|
|
96
|
+
|
|
97
|
+
## Usage
|
|
98
|
+
|
|
99
|
+
Below is a quick example how to use _qvdjs_.
|
|
100
|
+
|
|
101
|
+
```javascript
|
|
102
|
+
import {QvdDataFrame} from 'qvdjs';
|
|
103
|
+
|
|
104
|
+
const df = await QvdDataFrame.fromQvd('path/to/file.qvd');
|
|
105
|
+
console.log(df.head(5));
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
The above example loads the _qvdjs_ library and parses an example QVD file. A QVD file is typically loaded using the static
|
|
109
|
+
`QvdDataFrame.fromQvd` function of the `QvdDataFrame` class itself. After loading the file's content, numerous methods and properties are available to work with the parsed data.
|
|
110
|
+
|
|
111
|
+
### Lazy Loading
|
|
112
|
+
|
|
113
|
+
For large QVD files, you can load only a specific number of rows to improve performance and reduce memory usage. The library implements **true lazy loading** - it reads only the necessary portions of the file from disk, not the entire file.
|
|
114
|
+
|
|
115
|
+
```javascript
|
|
116
|
+
import {QvdDataFrame} from 'qvdjs';
|
|
117
|
+
|
|
118
|
+
// Load only the first 1000 rows
|
|
119
|
+
const df = await QvdDataFrame.fromQvd('path/to/file.qvd', {maxRows: 1000});
|
|
120
|
+
console.log(df.shape); // [1000, numberOfColumns]
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
**How it works:**
|
|
124
|
+
|
|
125
|
+
- The library reads only the header, symbol table, and the first N rows from the index table
|
|
126
|
+
- For a 5 GB file with `maxRows: 25`, only about 35-40% of the file is read from disk (~1.75-2 GB)
|
|
127
|
+
- This provides significant memory savings and faster loading times for large files
|
|
128
|
+
|
|
129
|
+
This is particularly useful for:
|
|
130
|
+
|
|
131
|
+
- Previewing data from very large QVD files without loading the entire file into memory
|
|
132
|
+
- Reducing memory consumption when working with multi-gigabyte files
|
|
133
|
+
- Faster loading times when you only need a subset of the data
|
|
134
|
+
- Data exploration and schema inspection of large datasets
|
|
135
|
+
|
|
136
|
+
### Progress Tracking for Large File Writes
|
|
137
|
+
|
|
138
|
+
When writing large QVD files (e.g., 100K+ rows), the `toQvd()` operation can take significant time. The library provides optional progress callbacks to track the write operation in real-time:
|
|
139
|
+
|
|
140
|
+
```javascript
|
|
141
|
+
import {QvdDataFrame} from 'qvdjs';
|
|
142
|
+
|
|
143
|
+
const df = await QvdDataFrame.fromDict({
|
|
144
|
+
columns: ['ID', 'Name', 'Value'],
|
|
145
|
+
data: largeDataArray, // e.g., 100,000+ rows
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
await df.toQvd('output.qvd', {
|
|
149
|
+
onProgress: (progress) => {
|
|
150
|
+
console.log(`${progress.stage}: ${progress.percent}% complete`);
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
**Progress Object Properties:**
|
|
156
|
+
|
|
157
|
+
The `progress` parameter passed to the `onProgress` callback contains the following properties:
|
|
158
|
+
|
|
159
|
+
- `stage` (string): Current operation stage being performed
|
|
160
|
+
- `current` (number): Current progress value (e.g., rows processed, columns completed)
|
|
161
|
+
- `total` (number): Total progress value for the current stage
|
|
162
|
+
- `percent` (number): Progress percentage (0-100) calculated as `(current / total) * 100`
|
|
163
|
+
|
|
164
|
+
**Progress stages:**
|
|
165
|
+
|
|
166
|
+
- `symbol-table`: Building unique value tables for each column
|
|
167
|
+
- `index-table`: Processing all data rows and creating index mappings
|
|
168
|
+
- `header`: Generating XML header metadata
|
|
169
|
+
- `write`: Writing data to disk
|
|
170
|
+
|
|
171
|
+
**Performance optimizations:**
|
|
172
|
+
|
|
173
|
+
The library uses optimized algorithms for large dataset processing:
|
|
174
|
+
|
|
175
|
+
- **Single-pass symbol table building**: All columns are processed in one pass through the data (previously required one pass per column)
|
|
176
|
+
- **Map-based lookups**: O(1) symbol index lookups instead of O(n) findIndex operations
|
|
177
|
+
- **Reduced algorithmic complexity**: From O(n×m×s) to O(n×m) where n=rows, m=columns, s=symbols
|
|
178
|
+
|
|
179
|
+
These optimizations can reduce write times by 80-90% for large datasets (100K+ rows).
|
|
180
|
+
|
|
181
|
+
### Working with Metadata
|
|
182
|
+
|
|
183
|
+
The library provides full access to QVD file and field metadata:
|
|
184
|
+
|
|
185
|
+
```javascript
|
|
186
|
+
import {QvdDataFrame} from 'qvdjs';
|
|
187
|
+
|
|
188
|
+
// Load QVD file
|
|
189
|
+
const df = await QvdDataFrame.fromQvd('path/to/file.qvd');
|
|
190
|
+
|
|
191
|
+
// Access file-level metadata
|
|
192
|
+
console.log(df.fileMetadata.tableName);
|
|
193
|
+
console.log(df.fileMetadata.createUtcTime);
|
|
194
|
+
console.log(df.fileMetadata.noOfRecords);
|
|
195
|
+
|
|
196
|
+
// Access field-level metadata
|
|
197
|
+
const fieldMeta = df.getFieldMetadata('ProductKey');
|
|
198
|
+
console.log(fieldMeta.comment);
|
|
199
|
+
console.log(fieldMeta.numberFormat);
|
|
200
|
+
console.log(fieldMeta.tags);
|
|
201
|
+
|
|
202
|
+
// Get all field metadata
|
|
203
|
+
const allFields = df.getAllFieldMetadata();
|
|
204
|
+
allFields.forEach((field) => {
|
|
205
|
+
console.log(`${field.fieldName}: ${field.noOfSymbols} symbols`);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// Modify metadata (only modifiable properties can be changed)
|
|
209
|
+
df.setFileMetadata({
|
|
210
|
+
tableName: 'UpdatedProducts',
|
|
211
|
+
comment: 'Modified product data',
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
df.setFieldMetadata('ProductKey', {
|
|
215
|
+
comment: 'Primary key for products',
|
|
216
|
+
tags: {String: ['$key', '$numeric']},
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// Metadata is preserved when writing
|
|
220
|
+
await df.toQvd('path/to/output.qvd');
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Security Considerations
|
|
224
|
+
|
|
225
|
+
The library includes built-in protection against path traversal attacks.
|
|
226
|
+
|
|
227
|
+
**Default Security Behavior:**
|
|
228
|
+
|
|
229
|
+
By default, all file operations are restricted to the **current working directory (CWD)** and its subdirectories. This means:
|
|
230
|
+
|
|
231
|
+
- ✅ Files within CWD can be accessed: `./data/file.qvd` or `data/file.qvd`
|
|
232
|
+
- ❌ Files outside CWD are blocked: `/etc/passwd` or `../../../sensitive.qvd`
|
|
233
|
+
- ❌ Path traversal attempts are detected and blocked
|
|
234
|
+
|
|
235
|
+
This default behavior protects against path traversal attacks without requiring additional configuration.
|
|
236
|
+
|
|
237
|
+
**Custom Directory Restriction:**
|
|
238
|
+
|
|
239
|
+
You can explicitly specify a different base directory using the `allowedDir` option:
|
|
240
|
+
|
|
241
|
+
```javascript
|
|
242
|
+
import {QvdDataFrame} from 'qvdjs';
|
|
243
|
+
|
|
244
|
+
// Restrict reading to a specific directory
|
|
245
|
+
const allowedDataDir = '/var/data/qvd-files';
|
|
246
|
+
const df = await QvdDataFrame.fromQvd('reports/sales.qvd', {
|
|
247
|
+
allowedDir: allowedDataDir,
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// Restrict writing to a specific directory
|
|
251
|
+
const allowedOutputDir = '/var/output';
|
|
252
|
+
await df.toQvd('processed/sales-filtered.qvd', {
|
|
253
|
+
allowedDir: allowedOutputDir,
|
|
254
|
+
});
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
**Security Features:**
|
|
258
|
+
|
|
259
|
+
- **Path Normalization**: All paths are automatically normalized using `path.resolve()` to eliminate `..` and `.` segments
|
|
260
|
+
- **Null Byte Protection**: Detects and blocks null byte injection attempts
|
|
261
|
+
- **Default CWD Restriction**: By default, file operations are restricted to the current working directory (CWD) and its subdirectories to prevent path traversal attacks
|
|
262
|
+
- **Custom Directory Restriction**: Optional `allowedDir` parameter allows you to specify a different base directory
|
|
263
|
+
- **Security Errors**: Throws `QvdSecurityError` with detailed context when security violations are detected
|
|
264
|
+
|
|
265
|
+
**Best Practices:**
|
|
266
|
+
|
|
267
|
+
1. **Understand default security**: Files are restricted to CWD by default - no additional configuration needed for basic protection
|
|
268
|
+
2. **Use explicit `allowedDir` in production**: When your application's CWD differs from your data directory, specify an explicit `allowedDir` for user-provided file paths
|
|
269
|
+
3. **Validate user input**: Even with built-in protections, validate and sanitize any user-provided paths
|
|
270
|
+
4. **Principle of least privilege**: Use the most restrictive `allowedDir` possible for your use case
|
|
271
|
+
5. **Monitor security errors**: Log and monitor `QvdSecurityError` exceptions as they may indicate attack attempts
|
|
272
|
+
|
|
273
|
+
**Examples:**
|
|
274
|
+
|
|
275
|
+
```javascript
|
|
276
|
+
import {QvdDataFrame, QvdSecurityError} from 'qvdjs';
|
|
277
|
+
|
|
278
|
+
// Example 1: Default behavior (restricted to CWD)
|
|
279
|
+
// This is safe by default - no path traversal possible
|
|
280
|
+
try {
|
|
281
|
+
const df = await QvdDataFrame.fromQvd('data/file.qvd'); // ✅ Works (within CWD)
|
|
282
|
+
const df2 = await QvdDataFrame.fromQvd('../../../etc/passwd'); // ❌ Throws QvdSecurityError
|
|
283
|
+
} catch (error) {
|
|
284
|
+
if (error instanceof QvdSecurityError) {
|
|
285
|
+
console.error('Path traversal blocked:', error.message);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Example 2: Custom allowedDir for specific use cases
|
|
290
|
+
try {
|
|
291
|
+
const df = await QvdDataFrame.fromQvd(userProvidedPath, {
|
|
292
|
+
allowedDir: '/safe/directory',
|
|
293
|
+
});
|
|
294
|
+
// Process data...
|
|
295
|
+
} catch (error) {
|
|
296
|
+
if (error instanceof QvdSecurityError) {
|
|
297
|
+
console.error('Security violation detected:', error.message);
|
|
298
|
+
console.error('Context:', error.context);
|
|
299
|
+
// Log security incident
|
|
300
|
+
} else {
|
|
301
|
+
throw error;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
## QVD File Format
|
|
307
|
+
|
|
308
|
+
The QVD file format is a binary file format that is used by QlikView to store data. The format is proprietary. However,
|
|
309
|
+
the format is well documented and can be parsed without the need of a QlikView installation. In fact, a QVD file consists
|
|
310
|
+
of three parts: a XML header, and two binary parts, the symbol and the index table. The XML header contains meta information
|
|
311
|
+
about the QVD file, such as the number of data records and the names of the fields. The symbol table contains the actual
|
|
312
|
+
distinct values of the fields. The index table contains the actual data records. The index table is a list of indices
|
|
313
|
+
which point to values in the symbol table.
|
|
314
|
+
|
|
315
|
+
### XML Header
|
|
316
|
+
|
|
317
|
+
The XML header contains meta information about the QVD file. The header is always located at the beginning of the file and
|
|
318
|
+
is in human readable text format. The header contains information about the number of data records, the names of the fields,
|
|
319
|
+
and the data types of the fields.
|
|
320
|
+
|
|
321
|
+
### Symbol Table
|
|
322
|
+
|
|
323
|
+
The symbol table contains the distinct/unique values of the fields and is located directly after the XML header. The order
|
|
324
|
+
of columns in the symbol table corresponds to the order of the fields in the XML header. The length and offset of the
|
|
325
|
+
symbol sections of each column are also stored in the XML header. Each symbol section consist of the unique symbols of the
|
|
326
|
+
respective column. The type of a single symbol is determined by a type byte prefixed to the respective symbol value. The
|
|
327
|
+
following type of symbols are supported:
|
|
328
|
+
|
|
329
|
+
| Code | Type | Description |
|
|
330
|
+
| ---- | ------------ | --------------------------------------------------------------------------------------------- |
|
|
331
|
+
| 1 | Integer | signed 4-byte integer (little endian) |
|
|
332
|
+
| 2 | Float | signed 8-byte IEEE floating point number (little endian) |
|
|
333
|
+
| 4 | String | null terminated string |
|
|
334
|
+
| 5 | Dual Integer | signed 4-byte integer (little endian) followed by a null terminated string |
|
|
335
|
+
| 6 | Dual Float | signed 8-byte IEEE floating point number (little endian) followed by a null terminated string |
|
|
336
|
+
|
|
337
|
+
### Index Table
|
|
338
|
+
|
|
339
|
+
After the symbol table, the index table follows. The index table contains the actual data records. The index table contains
|
|
340
|
+
binary indices that refrences to the values of each row in the symbol table. The order of the columns in the index table
|
|
341
|
+
corresponds to the order of the fields in the XML header. Hence, the index table does not contain the actual values of a
|
|
342
|
+
data record, but only the indices that point to the values in the symbol table.
|
|
343
|
+
|
|
344
|
+
### Empty QVD Files
|
|
345
|
+
|
|
346
|
+
QVD files can be empty in the sense that they contain zero data rows while still maintaining valid field definitions and metadata. This is a valid use case in Qlik applications where table structures need to be preserved even when no data is available.
|
|
347
|
+
|
|
348
|
+
**Characteristics of Empty QVD Files:**
|
|
349
|
+
|
|
350
|
+
- **NoOfRecords**: Set to `0` in the XML header
|
|
351
|
+
- **RecordByteSize**: Set to `1` (following Qlik Sense's convention)
|
|
352
|
+
- **Fields**: Field definitions are present and complete with metadata
|
|
353
|
+
- **Symbol Table**: Each field has zero symbols (`NoOfSymbols=0`, `Offset=0`, `Length=0`)
|
|
354
|
+
- **Index Table**: Empty with `Length=0`
|
|
355
|
+
- **BitWidth**: Can vary per field (typically `0` or `8`)
|
|
356
|
+
|
|
357
|
+
**Example Empty QVD XML Header:**
|
|
358
|
+
|
|
359
|
+
```xml
|
|
360
|
+
<QvdTableHeader>
|
|
361
|
+
<TableName>EmptyTable</TableName>
|
|
362
|
+
<Fields>
|
|
363
|
+
<QvdFieldHeader>
|
|
364
|
+
<FieldName>Country</FieldName>
|
|
365
|
+
<BitOffset>0</BitOffset>
|
|
366
|
+
<BitWidth>0</BitWidth>
|
|
367
|
+
<Bias>0</Bias>
|
|
368
|
+
<NoOfSymbols>0</NoOfSymbols>
|
|
369
|
+
<Offset>0</Offset>
|
|
370
|
+
<Length>0</Length>
|
|
371
|
+
</QvdFieldHeader>
|
|
372
|
+
</Fields>
|
|
373
|
+
<RecordByteSize>1</RecordByteSize>
|
|
374
|
+
<NoOfRecords>0</NoOfRecords>
|
|
375
|
+
<Offset>0</Offset>
|
|
376
|
+
<Length>0</Length>
|
|
377
|
+
</QvdTableHeader>
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
**Working with Empty QVDs:**
|
|
381
|
+
|
|
382
|
+
```javascript
|
|
383
|
+
import {QvdDataFrame} from 'qvdjs';
|
|
384
|
+
|
|
385
|
+
// Create an empty QVD with field structure but no data
|
|
386
|
+
const emptyDf = new QvdDataFrame(
|
|
387
|
+
[], // No data rows
|
|
388
|
+
['Country', 'Year', 'Sales'], // Column definitions
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
// Set metadata
|
|
392
|
+
emptyDf.setFileMetadata({
|
|
393
|
+
tableName: 'EmptyTable',
|
|
394
|
+
comment: 'Template table structure',
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
// Write empty QVD (compatible with Qlik Sense)
|
|
398
|
+
await emptyDf.toQvd('empty.qvd');
|
|
399
|
+
|
|
400
|
+
// Read it back
|
|
401
|
+
const loadedDf = await QvdDataFrame.fromQvd('empty.qvd');
|
|
402
|
+
console.log(loadedDf.shape); // [0, 3] - zero rows, three columns
|
|
403
|
+
console.log(loadedDf.columns); // ['Country', 'Year', 'Sales']
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
Empty QVDs are fully supported for both reading and writing, maintaining compatibility with files created by Qlik Sense and QlikView.
|
|
407
|
+
|
|
408
|
+
## API Documentation
|
|
409
|
+
|
|
410
|
+
### QvdDataFrame
|
|
411
|
+
|
|
412
|
+
The `QvdDataFrame` class represents the data frame stored inside of a finally parsed QVD file. It provides a high-level
|
|
413
|
+
abstraction access to the QVD file content. This includes meta information as well as access to the actual data records.
|
|
414
|
+
|
|
415
|
+
| Property | Type | Description |
|
|
416
|
+
| -------------- | ---------- | ------------------------------------------------------------------------------------------------------------------ |
|
|
417
|
+
| `shape` | `number[]` | The shape of the data table. The first element is the number of rows, the second element is the number of columns. |
|
|
418
|
+
| `data` | `any[][]` | The actual data records of the QVD file. The first dimension represents the single rows. |
|
|
419
|
+
| `columns` | `string[]` | The names of the fields that are contained in the QVD file. |
|
|
420
|
+
| `metadata` | `object` | The complete metadata object from the QVD file header, or null if not loaded from a QVD file. |
|
|
421
|
+
| `fileMetadata` | `object` | File-level metadata from the QVD header (qvBuildNo, tableName, createUtcTime, etc.). |
|
|
422
|
+
|
|
423
|
+
#### `static fromQvd(path: string, options?: object): Promise<QvdDataFrame>`
|
|
424
|
+
|
|
425
|
+
The static method `QvdDataFrame.fromQvd` loads a QVD file from the given path and parses it. The method returns a promise that resolves
|
|
426
|
+
to a `QvdDataFrame` instance.
|
|
427
|
+
|
|
428
|
+
**Parameters:**
|
|
429
|
+
|
|
430
|
+
- `path` (string): The path to the QVD file.
|
|
431
|
+
- `options` (object, optional): Loading options
|
|
432
|
+
- `maxRows` (number, optional): Maximum number of rows to load. If not specified, all rows are loaded. This is useful for loading only a subset of data from large QVD files to improve performance and reduce memory usage.
|
|
433
|
+
- `allowedDir` (string, optional): Base directory for file access validation. Defaults to current working directory (CWD). The file path must resolve to a location within this directory to prevent path traversal attacks. Set to a specific directory in production environments with user-provided paths.
|
|
434
|
+
|
|
435
|
+
**Example:**
|
|
436
|
+
|
|
437
|
+
```javascript
|
|
438
|
+
// Load all rows (default behavior)
|
|
439
|
+
const df = await QvdDataFrame.fromQvd('path/to/file.qvd');
|
|
440
|
+
|
|
441
|
+
// Load only the first 1000 rows
|
|
442
|
+
const dfLazy = await QvdDataFrame.fromQvd('path/to/file.qvd', {maxRows: 1000});
|
|
443
|
+
|
|
444
|
+
// Load with security restriction (recommended for production)
|
|
445
|
+
const dfSecure = await QvdDataFrame.fromQvd('reports/sales.qvd', {
|
|
446
|
+
allowedDir: '/var/data/qvd-files',
|
|
447
|
+
});
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
#### `static fromDict(dict: object): Promise<QvdDataFrame>`
|
|
451
|
+
|
|
452
|
+
The static method `QvdDataFrame.fromDict` constructs a data frame from a dictionary. The dictionary must contain the columns and
|
|
453
|
+
the actual data as properties. The columns property is an array of strings that contains the names of the fields in the QVD file.
|
|
454
|
+
The data property is an array of arrays that contains the actual data records. The order of the values in the inner arrays
|
|
455
|
+
corresponds to the order of the fields in the QVD file.
|
|
456
|
+
|
|
457
|
+
#### `head(n: number): QvdDataFrame`
|
|
458
|
+
|
|
459
|
+
The method `head` returns the first `n` rows of the data frame.
|
|
460
|
+
|
|
461
|
+
#### `tail(n: number): QvdDataFrame`
|
|
462
|
+
|
|
463
|
+
The method `tail` returns the last `n` rows of the data frame.
|
|
464
|
+
|
|
465
|
+
#### `rows(...args: number): QvdDataFrame`
|
|
466
|
+
|
|
467
|
+
The method `rows` returns a new data frame that contains only the specified rows.
|
|
468
|
+
|
|
469
|
+
#### `at(row: number, column: string): any`
|
|
470
|
+
|
|
471
|
+
The method `at` returns the value at the specified row and column.
|
|
472
|
+
|
|
473
|
+
#### `select(...args: string): QvdDataFrame`
|
|
474
|
+
|
|
475
|
+
The method `select` returns a new data frame that contains only the specified columns.
|
|
476
|
+
|
|
477
|
+
#### `toDict(): Promise<object>`
|
|
478
|
+
|
|
479
|
+
The method `toDict` returns the data frame as a dictionary. The dictionary contains the columns and the
|
|
480
|
+
actual data as properties. The columns property is an array of strings that contains the names of the
|
|
481
|
+
fields in the QVD file. The data property is an array of arrays that contains the actual data records.
|
|
482
|
+
The order of the values in the inner arrays corresponds to the order of the fields in the QVD file.
|
|
483
|
+
|
|
484
|
+
#### `toQvd(path: string, options?: object): Promise<void>`
|
|
485
|
+
|
|
486
|
+
The method `toQvd` writes the data frame to a QVD file at the specified path.
|
|
487
|
+
|
|
488
|
+
**Parameters:**
|
|
489
|
+
|
|
490
|
+
- `path` (string): The path where the QVD file should be written.
|
|
491
|
+
- `options` (object, optional): Writing options
|
|
492
|
+
- `allowedDir` (string, optional): Base directory for file write validation. Defaults to current working directory (CWD). The file path must resolve to a location within this directory to prevent path traversal attacks. Set to a specific directory in production environments with user-provided paths.
|
|
493
|
+
- `onProgress` (function, optional): Progress callback function for tracking write operations. Receives progress updates during symbol table building, index table building, header generation, and data writing.
|
|
494
|
+
|
|
495
|
+
**Progress Callback:**
|
|
496
|
+
|
|
497
|
+
The `onProgress` callback receives an object with the following properties:
|
|
498
|
+
|
|
499
|
+
- `stage` (string): Current operation stage - `'symbol-table'`, `'index-table'`, `'header'`, or `'write'`
|
|
500
|
+
- `current` (number): Current progress value (e.g., rows processed, columns completed)
|
|
501
|
+
- `total` (number): Total progress value
|
|
502
|
+
- `percent` (number): Progress percentage (0-100)
|
|
503
|
+
|
|
504
|
+
This is particularly useful for large QVD files where write operations can take significant time.
|
|
505
|
+
|
|
506
|
+
**Examples:**
|
|
507
|
+
|
|
508
|
+
```javascript
|
|
509
|
+
// Write to file (default behavior)
|
|
510
|
+
await df.toQvd('output/data.qvd');
|
|
511
|
+
|
|
512
|
+
// Write with security restriction (recommended for production)
|
|
513
|
+
await df.toQvd('processed/data.qvd', {
|
|
514
|
+
allowedDir: '/var/output/qvd-files',
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
// Write with progress tracking for large files
|
|
518
|
+
await df.toQvd('large-output.qvd', {
|
|
519
|
+
onProgress: (progress) => {
|
|
520
|
+
console.log(`${progress.stage}: ${progress.percent}% (${progress.current}/${progress.total})`);
|
|
521
|
+
},
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
// Write with detailed progress bar
|
|
525
|
+
await df.toQvd('data.qvd', {
|
|
526
|
+
onProgress: (progress) => {
|
|
527
|
+
const bar = '█'.repeat(Math.floor(progress.percent / 2)) + '░'.repeat(50 - Math.floor(progress.percent / 2));
|
|
528
|
+
process.stdout.write(`\r[${progress.stage}] ${bar} ${progress.percent}%`);
|
|
529
|
+
if (progress.current === progress.total) console.log(' ✓');
|
|
530
|
+
},
|
|
531
|
+
});
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
#### `getFieldMetadata(fieldName: string): object | null`
|
|
535
|
+
|
|
536
|
+
The method `getFieldMetadata` returns the metadata for a specific field/column from the QVD header. Returns null if the field is not found or metadata is not available.
|
|
537
|
+
|
|
538
|
+
The returned object contains:
|
|
539
|
+
|
|
540
|
+
- `fieldName`: Name of the field
|
|
541
|
+
- `bitOffset`: Bit offset in the index table
|
|
542
|
+
- `bitWidth`: Bit width in the index table
|
|
543
|
+
- `bias`: Bias value for index calculation
|
|
544
|
+
- `noOfSymbols`: Number of unique symbols/values
|
|
545
|
+
- `offset`: Byte offset in the symbol table
|
|
546
|
+
- `length`: Byte length in the symbol table
|
|
547
|
+
- `comment`: Field comment (modifiable)
|
|
548
|
+
- `numberFormat`: Number format settings (modifiable)
|
|
549
|
+
- `tags`: Field tags (modifiable)
|
|
550
|
+
|
|
551
|
+
Note: Properties like `offset`, `length`, `bitOffset`, `bitWidth`, `bias`, and `noOfSymbols` are immutable and relate to internal data storage.
|
|
552
|
+
|
|
553
|
+
#### `getAllFieldMetadata(): object[]`
|
|
554
|
+
|
|
555
|
+
The method `getAllFieldMetadata` returns an array of metadata objects for all fields in the data frame. Each object has the same structure as returned by `getFieldMetadata`.
|
|
556
|
+
|
|
557
|
+
#### `setFileMetadata(metadata: object): void`
|
|
558
|
+
|
|
559
|
+
The method `setFileMetadata` allows modifying file-level metadata. Only modifiable properties are updated; immutable properties related to data storage are ignored.
|
|
560
|
+
|
|
561
|
+
Modifiable properties:
|
|
562
|
+
|
|
563
|
+
- `qvBuildNo`: QlikView build number
|
|
564
|
+
- `creatorDoc`: Document GUID that created the QVD
|
|
565
|
+
- `createUtcTime`: Creation timestamp
|
|
566
|
+
- `sourceCreateUtcTime`: Source creation timestamp
|
|
567
|
+
- `sourceFileUtcTime`: Source file timestamp
|
|
568
|
+
- `sourceFileSize`: Source file size
|
|
569
|
+
- `staleUtcTime`: Stale timestamp
|
|
570
|
+
- `tableName`: Table name
|
|
571
|
+
- `compression`: Compression method
|
|
572
|
+
- `comment`: Table comment
|
|
573
|
+
- `encryptionInfo`: Encryption information
|
|
574
|
+
- `tableTags`: Table tags
|
|
575
|
+
- `profilingData`: Profiling data
|
|
576
|
+
- `lineage`: Data lineage information
|
|
577
|
+
|
|
578
|
+
Immutable properties (cannot be modified):
|
|
579
|
+
|
|
580
|
+
- `noOfRecords`: Number of records
|
|
581
|
+
- `recordByteSize`: Record byte size
|
|
582
|
+
- `offset`: Byte offset in file
|
|
583
|
+
- `length`: Byte length in file
|
|
584
|
+
|
|
585
|
+
#### `setFieldMetadata(fieldName: string, metadata: object): void`
|
|
586
|
+
|
|
587
|
+
The method `setFieldMetadata` allows modifying field-level metadata for a specific field. Only modifiable properties are updated; immutable properties related to data storage are ignored.
|
|
588
|
+
|
|
589
|
+
Modifiable properties:
|
|
590
|
+
|
|
591
|
+
- `comment`: Field comment/description
|
|
592
|
+
- `numberFormat`: Number format settings (Type, nDec, UseThou, Fmt, Dec, Thou)
|
|
593
|
+
- `tags`: Field tags (typically used for field classification)
|
|
594
|
+
|
|
595
|
+
Immutable properties (cannot be modified):
|
|
596
|
+
|
|
597
|
+
- `offset`: Byte offset in symbol table
|
|
598
|
+
- `length`: Byte length in symbol table
|
|
599
|
+
- `bitOffset`: Bit offset in index table
|
|
600
|
+
- `bitWidth`: Bit width in index table
|
|
601
|
+
- `bias`: Bias value
|
|
602
|
+
- `noOfSymbols`: Number of symbols
|
|
603
|
+
|
|
604
|
+
## Documentation
|
|
605
|
+
|
|
606
|
+
Comprehensive documentation is available to help you understand, use, and contribute to qvdjs:
|
|
607
|
+
|
|
608
|
+
### For Users
|
|
609
|
+
|
|
610
|
+
- **[README.md](./README.md)** (this file) - Quick start guide and API reference
|
|
611
|
+
- **[QVD_FORMAT.md](./QVD_FORMAT.md)** - Complete QVD file format specification
|
|
612
|
+
- Binary structure details
|
|
613
|
+
- Symbol type encodings
|
|
614
|
+
- Bit packing algorithms
|
|
615
|
+
- Example file breakdowns
|
|
616
|
+
|
|
617
|
+
### For Contributors
|
|
618
|
+
|
|
619
|
+
- **[CONTRIBUTING.md](./CONTRIBUTING.md)** - How to contribute to the project
|
|
620
|
+
- Development setup
|
|
621
|
+
- Code style guidelines
|
|
622
|
+
- Commit message conventions
|
|
623
|
+
- Pull request process
|
|
624
|
+
- Bug reporting and feature requests
|
|
625
|
+
|
|
626
|
+
- **[DEVELOPMENT.md](./DEVELOPMENT.md)** - Technical development guide
|
|
627
|
+
- Architecture overview
|
|
628
|
+
- Implementation details
|
|
629
|
+
- Design patterns
|
|
630
|
+
- Performance considerations
|
|
631
|
+
- Error handling strategies
|
|
632
|
+
- Debugging tips
|
|
633
|
+
|
|
634
|
+
- **[ARCHITECTURE.md](./ARCHITECTURE.md)** - High-level architecture
|
|
635
|
+
- Component diagrams
|
|
636
|
+
- Class relationships
|
|
637
|
+
- Data flow visualization
|
|
638
|
+
- Design decisions and rationale
|
|
639
|
+
- Extension points
|
|
640
|
+
- Future considerations
|
|
641
|
+
|
|
642
|
+
- **[docs/TESTING.md](./docs/TESTING.md)** - Comprehensive testing guide
|
|
643
|
+
- Testing philosophy and strategy
|
|
644
|
+
- How to write unit and integration tests
|
|
645
|
+
- Performance testing guidelines
|
|
646
|
+
- Coverage targets
|
|
647
|
+
- Multi-platform testing setup
|
|
648
|
+
|
|
649
|
+
### Quick Links by Task
|
|
650
|
+
|
|
651
|
+
| I want to... | See... |
|
|
652
|
+
| ----------------------- | ------------------------------------------------------------------------- |
|
|
653
|
+
| Use the library | [README.md](./README.md) - Usage section |
|
|
654
|
+
| Understand QVD format | [QVD_FORMAT.md](./QVD_FORMAT.md) |
|
|
655
|
+
| Report a bug | [CONTRIBUTING.md](./CONTRIBUTING.md#reporting-bugs) |
|
|
656
|
+
| Suggest a feature | [CONTRIBUTING.md](./CONTRIBUTING.md#suggesting-features) |
|
|
657
|
+
| Contribute code | [CONTRIBUTING.md](./CONTRIBUTING.md) + [DEVELOPMENT.md](./DEVELOPMENT.md) |
|
|
658
|
+
| Understand architecture | [ARCHITECTURE.md](./ARCHITECTURE.md) |
|
|
659
|
+
| Write tests | [docs/TESTING.md](./docs/TESTING.md) |
|
|
660
|
+
| Debug an issue | [DEVELOPMENT.md](./DEVELOPMENT.md#debugging-tips) |
|
|
661
|
+
|
|
662
|
+
## Testing
|
|
663
|
+
|
|
664
|
+
qvdjs has comprehensive test coverage with automated multi-platform testing. For detailed information about the testing infrastructure, including:
|
|
665
|
+
|
|
666
|
+
- Test architecture and coverage breakdown
|
|
667
|
+
- Multi-platform support (Windows, macOS, Linux)
|
|
668
|
+
- Security testing approach
|
|
669
|
+
- Self-hosted runner setup
|
|
670
|
+
- Performance benchmarking
|
|
671
|
+
|
|
672
|
+
See the **[Testing Documentation](./docs/README.md)** in the `docs/` directory.
|
|
673
|
+
|
|
674
|
+
Quick links:
|
|
675
|
+
|
|
676
|
+
- **[Testing Summary](./docs/TESTING_SUMMARY.md)** - Executive overview and quick start
|
|
677
|
+
- **[Complete Design](./docs/MULTI_PLATFORM_TEST_DESIGN.md)** - Full technical specification
|
|
678
|
+
|
|
679
|
+
### Running Tests
|
|
680
|
+
|
|
681
|
+
```bash
|
|
682
|
+
# Run all tests
|
|
683
|
+
npm test
|
|
684
|
+
|
|
685
|
+
# Run tests with coverage
|
|
686
|
+
npm run coverage
|
|
687
|
+
|
|
688
|
+
# Run specific test file
|
|
689
|
+
npm test -- __tests__/reader.test.js
|
|
690
|
+
```
|
|
691
|
+
|
|
692
|
+
Current test coverage: **91.7%** across 95+ tests covering unit, integration, and error handling scenarios.
|
|
693
|
+
|
|
694
|
+
## Contributing
|
|
695
|
+
|
|
696
|
+
We welcome contributions! When contributing to this project, please follow the [Conventional Commits](https://www.conventionalcommits.org/) specification for commit messages. This enables automatic version management and changelog generation.
|
|
697
|
+
|
|
698
|
+
For detailed information about the release process, see [RELEASING.md](docs/RELEASING.md).
|
|
699
|
+
|
|
700
|
+
## Contributors
|
|
701
|
+
|
|
702
|
+
- [Constantin Müller](https://mueller-constantin.de) - Original author
|
|
703
|
+
- [Göran Sander](https://github.com/mountaindude) - General refresh, improved error handling, expose all metadata from XML headers, lazy loading of symbol and index tables, ESM/CJS support, multi-platform testing, TypeScript typings, security hardening, bug fixes
|
|
704
|
+
|
|
705
|
+
## License
|
|
706
|
+
|
|
707
|
+
Copyright (c) 2024 Constantin Müller
|
|
708
|
+
|
|
709
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
710
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
711
|
+
in the Software without restriction, including without limitation the rights
|
|
712
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
713
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
714
|
+
furnished to do so, subject to the following conditions:
|
|
715
|
+
|
|
716
|
+
The above copyright notice and this permission notice shall be included in all
|
|
717
|
+
copies or substantial portions of the Software.
|
|
718
|
+
|
|
719
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
720
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
721
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
722
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
723
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
724
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
725
|
+
SOFTWARE.
|
|
726
|
+
|
|
727
|
+
[MIT License](https://opensource.org/licenses/MIT) or [LICENSE](LICENSE) for
|
|
728
|
+
more details.
|
|
729
|
+
|
|
730
|
+
### Forbidden
|
|
731
|
+
|
|
732
|
+
**Hold Liable**: Software is provided without warranty and the software
|
|
733
|
+
author/license owner cannot be held liable for damages.
|