add duckdb-ui-client & other ts pkgs (#10)
* add duckdb-ui-client & other ts pkgs * workflow fixes * fix working dir * no sparse checkout; specify package.json path * path to pnpm-lock.yaml * add check & build test * workflow step descriptions * use comments & names * one more naming tweak
This commit is contained in:
8
ts/pkgs/duckdb-ui-client/test/helpers/makeBuffer.ts
Normal file
8
ts/pkgs/duckdb-ui-client/test/helpers/makeBuffer.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export function makeBuffer(bytes: number[]): ArrayBuffer {
|
||||
const buffer = new ArrayBuffer(bytes.length);
|
||||
const dv = new DataView(buffer);
|
||||
for (let offset = 0; offset < bytes.length; offset++) {
|
||||
dv.setUint8(offset, bytes[offset]);
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
15
ts/pkgs/duckdb-ui-client/test/helpers/mockRequests.ts
Normal file
15
ts/pkgs/duckdb-ui-client/test/helpers/mockRequests.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { RequestHandler } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
|
||||
export async function mockRequests(
|
||||
handlers: RequestHandler[],
|
||||
func: () => Promise<void>,
|
||||
) {
|
||||
const server = setupServer(...handlers);
|
||||
try {
|
||||
server.listen();
|
||||
await func();
|
||||
} finally {
|
||||
server.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { expect, suite, test } from 'vitest';
|
||||
import { DuckDBUIHttpRequestQueue } from '../../../src/http/classes/DuckDBUIHttpRequestQueue';
|
||||
import { makeBuffer } from '../../helpers/makeBuffer';
|
||||
import { mockRequests } from '../../helpers/mockRequests';
|
||||
|
||||
suite('DuckDBUIHttpRequestQueue', () => {
|
||||
test('single request', () => {
|
||||
return mockRequests(
|
||||
[
|
||||
http.post('http://localhost/example/path', () => {
|
||||
return HttpResponse.arrayBuffer(makeBuffer([17, 42]));
|
||||
}),
|
||||
],
|
||||
async () => {
|
||||
const queue = new DuckDBUIHttpRequestQueue();
|
||||
const id = queue.enqueue(
|
||||
'http://localhost/example/path',
|
||||
'example body',
|
||||
);
|
||||
expect(queue.length).toBe(1);
|
||||
expect(queue.isCurrent(id)).toBe(true);
|
||||
|
||||
const result = await queue.enqueuedResult(id);
|
||||
expect(result.buffer).toEqual(makeBuffer([17, 42]));
|
||||
},
|
||||
);
|
||||
});
|
||||
test('multiple requests', () => {
|
||||
return mockRequests(
|
||||
[
|
||||
http.post('http://localhost/example/path', async ({ request }) => {
|
||||
const body = await request.text();
|
||||
const value = parseInt(body.split(' ')[0], 10);
|
||||
return HttpResponse.arrayBuffer(makeBuffer([value]));
|
||||
}),
|
||||
],
|
||||
async () => {
|
||||
const queue = new DuckDBUIHttpRequestQueue();
|
||||
const id1 = queue.enqueue(
|
||||
'http://localhost/example/path',
|
||||
'11 example body',
|
||||
);
|
||||
const id2 = queue.enqueue(
|
||||
'http://localhost/example/path',
|
||||
'22 example body',
|
||||
);
|
||||
expect(queue.length).toBe(2);
|
||||
expect(queue.isCurrent(id1)).toBe(true);
|
||||
|
||||
const result1 = await queue.enqueuedResult(id1);
|
||||
expect(result1.buffer).toEqual(makeBuffer([11]));
|
||||
|
||||
expect(queue.length).toBe(1);
|
||||
expect(queue.isCurrent(id2)).toBe(true);
|
||||
|
||||
const result2 = await queue.enqueuedResult(id2);
|
||||
expect(result2.buffer).toEqual(makeBuffer([22]));
|
||||
},
|
||||
);
|
||||
});
|
||||
test('cancel (first request)', () => {
|
||||
return mockRequests(
|
||||
[
|
||||
http.post('http://localhost/example/path', async ({ request }) => {
|
||||
const body = await request.text();
|
||||
const value = parseInt(body.split(' ')[0], 10);
|
||||
return HttpResponse.arrayBuffer(makeBuffer([value]));
|
||||
}),
|
||||
],
|
||||
async () => {
|
||||
const queue = new DuckDBUIHttpRequestQueue();
|
||||
const id1 = queue.enqueue(
|
||||
'http://localhost/example/path',
|
||||
'11 example body',
|
||||
);
|
||||
const id2 = queue.enqueue(
|
||||
'http://localhost/example/path',
|
||||
'22 example body',
|
||||
);
|
||||
expect(queue.length).toBe(2);
|
||||
expect(queue.isCurrent(id1)).toBe(true);
|
||||
|
||||
queue.cancel(id1);
|
||||
await expect(queue.enqueuedResult(id1)).rejects.toEqual(
|
||||
new Error('query was canceled'),
|
||||
);
|
||||
|
||||
const result2 = await queue.enqueuedResult(id2);
|
||||
expect(result2.buffer).toEqual(makeBuffer([22]));
|
||||
},
|
||||
);
|
||||
});
|
||||
test('cancel (second request)', () => {
|
||||
return mockRequests(
|
||||
[
|
||||
http.post('http://localhost/example/path', async ({ request }) => {
|
||||
const body = await request.text();
|
||||
const value = parseInt(body.split(' ')[0], 10);
|
||||
return HttpResponse.arrayBuffer(makeBuffer([value]));
|
||||
}),
|
||||
],
|
||||
async () => {
|
||||
const queue = new DuckDBUIHttpRequestQueue();
|
||||
const id1 = queue.enqueue(
|
||||
'http://localhost/example/path',
|
||||
'11 example body',
|
||||
);
|
||||
const id2 = queue.enqueue(
|
||||
'http://localhost/example/path',
|
||||
'22 example body',
|
||||
);
|
||||
const id3 = queue.enqueue(
|
||||
'http://localhost/example/path',
|
||||
'33 example body',
|
||||
);
|
||||
expect(queue.length).toBe(3);
|
||||
expect(queue.isCurrent(id1)).toBe(true);
|
||||
|
||||
const promise2 = queue.enqueuedResult(id2);
|
||||
queue.cancel(id2, 'example error message');
|
||||
|
||||
const result1 = await queue.enqueuedResult(id1);
|
||||
expect(result1.buffer).toEqual(makeBuffer([11]));
|
||||
|
||||
expect(queue.length).toBe(1);
|
||||
expect(queue.isCurrent(id3)).toBe(true);
|
||||
|
||||
await expect(promise2).rejects.toEqual(
|
||||
new Error('example error message'),
|
||||
);
|
||||
|
||||
const result3 = await queue.enqueuedResult(id3);
|
||||
expect(result3.buffer).toEqual(makeBuffer([33]));
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
import { expect, suite, test } from 'vitest';
|
||||
import { makeDuckDBUIHttpRequestHeaders } from '../../../src/http/functions/makeDuckDBUIHttpRequestHeaders';
|
||||
|
||||
suite('makeDuckDBUIHttpRequestHeaders', () => {
|
||||
test('description', () => {
|
||||
expect([
|
||||
...makeDuckDBUIHttpRequestHeaders({
|
||||
description: 'example description',
|
||||
}).entries(),
|
||||
]).toEqual([['x-duckdb-ui-request-description', 'example description']]);
|
||||
});
|
||||
test('connection name', () => {
|
||||
expect([
|
||||
...makeDuckDBUIHttpRequestHeaders({
|
||||
connectionName: 'example connection name',
|
||||
}).entries(),
|
||||
]).toEqual([['x-duckdb-ui-connection-name', 'example connection name']]);
|
||||
});
|
||||
test('database name', () => {
|
||||
// should be base64 encoded
|
||||
expect([
|
||||
...makeDuckDBUIHttpRequestHeaders({
|
||||
databaseName: 'example database name',
|
||||
}).entries(),
|
||||
]).toEqual([['x-duckdb-ui-database-name', 'ZXhhbXBsZSBkYXRhYmFzZSBuYW1l']]);
|
||||
});
|
||||
test('parameters', () => {
|
||||
// values should be base64 encoded
|
||||
expect([
|
||||
...makeDuckDBUIHttpRequestHeaders({
|
||||
parameters: ['first', 'second'],
|
||||
}).entries(),
|
||||
]).toEqual([
|
||||
['x-duckdb-ui-parameter-count', '2'],
|
||||
['x-duckdb-ui-parameter-value-0', 'Zmlyc3Q='],
|
||||
['x-duckdb-ui-parameter-value-1', 'c2Vjb25k'],
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { expect, suite, test } from 'vitest';
|
||||
import { sendDuckDBUIHttpRequest } from '../../../src/http/functions/sendDuckDBUIHttpRequest';
|
||||
import { makeBuffer } from '../../helpers/makeBuffer';
|
||||
import { mockRequests } from '../../helpers/mockRequests';
|
||||
|
||||
suite('sendDuckDBUIHttpRequest', () => {
|
||||
test('basic', async () => {
|
||||
return mockRequests(
|
||||
[
|
||||
http.post('http://localhost/example/path', () => {
|
||||
return HttpResponse.arrayBuffer(makeBuffer([17, 42]));
|
||||
}),
|
||||
],
|
||||
async () => {
|
||||
await expect(
|
||||
sendDuckDBUIHttpRequest(
|
||||
'http://localhost/example/path',
|
||||
'example body',
|
||||
),
|
||||
).resolves.toEqual(makeBuffer([17, 42]));
|
||||
},
|
||||
);
|
||||
});
|
||||
test('headers', async () => {
|
||||
return mockRequests(
|
||||
[
|
||||
http.post('http://localhost/example/path', ({ request }) => {
|
||||
if (
|
||||
request.headers.get('X-Example-Header-1') !==
|
||||
'example-header-1-value' ||
|
||||
request.headers.get('X-Example-Header-2') !==
|
||||
'example-header-2-value'
|
||||
) {
|
||||
return HttpResponse.error();
|
||||
}
|
||||
return HttpResponse.arrayBuffer(makeBuffer([17, 42]));
|
||||
}),
|
||||
],
|
||||
async () => {
|
||||
const headers = new Headers();
|
||||
headers.append('X-Example-Header-1', 'example-header-1-value');
|
||||
headers.append('X-Example-Header-2', 'example-header-2-value');
|
||||
await expect(
|
||||
sendDuckDBUIHttpRequest(
|
||||
'http://localhost/example/path',
|
||||
'example body',
|
||||
headers,
|
||||
),
|
||||
).resolves.toEqual(makeBuffer([17, 42]));
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,87 @@
|
||||
import { expect, suite, test } from 'vitest';
|
||||
import { BinaryDeserializer } from '../../../src/serialization/classes/BinaryDeserializer';
|
||||
import { BinaryStreamReader } from '../../../src/serialization/classes/BinaryStreamReader';
|
||||
import {
|
||||
readString,
|
||||
readUint8,
|
||||
} from '../../../src/serialization/functions/basicReaders';
|
||||
import { makeBuffer } from '../../helpers/makeBuffer';
|
||||
|
||||
suite('BinaryDeserializer', () => {
|
||||
test('read uint8', () => {
|
||||
const deserializer = new BinaryDeserializer(
|
||||
new BinaryStreamReader(makeBuffer([17, 42])),
|
||||
);
|
||||
expect(deserializer.readUint8()).toBe(17);
|
||||
expect(deserializer.readUint8()).toBe(42);
|
||||
});
|
||||
test('read varint', () => {
|
||||
const deserializer = new BinaryDeserializer(
|
||||
new BinaryStreamReader(makeBuffer([0x81, 0x82, 0x03])),
|
||||
);
|
||||
expect(deserializer.readVarInt()).toBe((3 << 14) | (2 << 7) | 1);
|
||||
});
|
||||
test('read nullable', () => {
|
||||
const deserializer = new BinaryDeserializer(
|
||||
new BinaryStreamReader(makeBuffer([0, 1, 17])),
|
||||
);
|
||||
expect(deserializer.readNullable(readUint8)).toBe(null);
|
||||
expect(deserializer.readNullable(readUint8)).toBe(17);
|
||||
});
|
||||
test('read data', () => {
|
||||
const deserializer = new BinaryDeserializer(
|
||||
new BinaryStreamReader(makeBuffer([3, 0xa, 0xb, 0xc])),
|
||||
);
|
||||
const dv = deserializer.readData();
|
||||
expect(dv.byteLength).toBe(3);
|
||||
expect(dv.getUint8(0)).toBe(0xa);
|
||||
expect(dv.getUint8(1)).toBe(0xb);
|
||||
expect(dv.getUint8(2)).toBe(0xc);
|
||||
});
|
||||
test('read string', () => {
|
||||
const deserializer = new BinaryDeserializer(
|
||||
new BinaryStreamReader(makeBuffer([4, 0x64, 0x75, 0x63, 0x6b])),
|
||||
);
|
||||
expect(deserializer.readString()).toBe('duck');
|
||||
});
|
||||
test('read list (of string)', () => {
|
||||
const deserializer = new BinaryDeserializer(
|
||||
new BinaryStreamReader(
|
||||
makeBuffer([
|
||||
3, 4, 0x77, 0x61, 0x6c, 0x6b, 4, 0x73, 0x77, 0x69, 0x6d, 3, 0x66,
|
||||
0x6c, 0x79,
|
||||
]),
|
||||
),
|
||||
);
|
||||
expect(deserializer.readList(readString)).toEqual(['walk', 'swim', 'fly']);
|
||||
});
|
||||
test('read pair', () => {
|
||||
const deserializer = new BinaryDeserializer(
|
||||
new BinaryStreamReader(
|
||||
makeBuffer([0, 0, 4, 0x64, 0x75, 0x63, 0x6b, 1, 0, 42, 0xff, 0xff]),
|
||||
),
|
||||
);
|
||||
expect(deserializer.readPair(readString, readUint8)).toEqual(['duck', 42]);
|
||||
});
|
||||
test('read property', () => {
|
||||
const deserializer = new BinaryDeserializer(
|
||||
new BinaryStreamReader(makeBuffer([100, 0, 4, 0x64, 0x75, 0x63, 0x6b])),
|
||||
);
|
||||
expect(deserializer.readProperty(100, readString)).toEqual('duck');
|
||||
});
|
||||
test('read property (not present)', () => {
|
||||
const deserializer = new BinaryDeserializer(
|
||||
new BinaryStreamReader(makeBuffer([100, 0, 4, 0x64, 0x75, 0x63, 0x6b])),
|
||||
);
|
||||
expect(() => deserializer.readProperty(101, readString)).toThrowError(
|
||||
'Expected field id 101 but got 100 (offset=0)',
|
||||
);
|
||||
});
|
||||
test('read property with default', () => {
|
||||
const deserializer = new BinaryDeserializer(
|
||||
new BinaryStreamReader(makeBuffer([101, 0, 42])),
|
||||
);
|
||||
expect(deserializer.readPropertyWithDefault(100, readUint8, 17)).toBe(17);
|
||||
expect(deserializer.readPropertyWithDefault(101, readUint8, 17)).toBe(42);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import { expect, suite, test } from 'vitest';
|
||||
import { BinaryStreamReader } from '../../../src/serialization/classes/BinaryStreamReader';
|
||||
import { makeBuffer } from '../../helpers/makeBuffer';
|
||||
|
||||
suite('BinaryStreamReader', () => {
|
||||
test('basic', () => {
|
||||
const reader = new BinaryStreamReader(
|
||||
makeBuffer([11, 22, 33, 44, 0x12, 0x34]),
|
||||
);
|
||||
|
||||
expect(reader.getOffset()).toBe(0);
|
||||
expect(reader.peekUint8()).toBe(11);
|
||||
expect(reader.readUint8()).toBe(11);
|
||||
|
||||
expect(reader.getOffset()).toBe(1);
|
||||
expect(reader.peekUint8()).toBe(22);
|
||||
expect(reader.readUint8()).toBe(22);
|
||||
|
||||
expect(reader.getOffset()).toBe(2);
|
||||
reader.consume(2);
|
||||
expect(reader.getOffset()).toBe(4);
|
||||
expect(reader.peekUint16(false)).toBe(0x1234);
|
||||
expect(reader.peekUint16(true)).toBe(0x3412);
|
||||
|
||||
const dv = reader.readData(2);
|
||||
expect(dv.byteLength).toBe(2);
|
||||
expect(dv.getUint8(0)).toBe(0x12);
|
||||
expect(dv.getUint8(1)).toBe(0x34);
|
||||
});
|
||||
});
|
||||
6
ts/pkgs/duckdb-ui-client/test/tsconfig.json
Normal file
6
ts/pkgs/duckdb-ui-client/test/tsconfig.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.test.json",
|
||||
"references": [
|
||||
{ "path": "../src" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { expect, suite, test } from 'vitest';
|
||||
import { randomString } from '../../../src/util/functions/randomString';
|
||||
|
||||
suite('randomString', () => {
|
||||
test('default length', () => {
|
||||
expect(randomString().length).toBe(12);
|
||||
});
|
||||
test('custom length', () => {
|
||||
expect(randomString(5).length).toBe(5);
|
||||
});
|
||||
test('custom chars', () => {
|
||||
expect(randomString(3, 'xy')).toMatch(/[xy][xy][xy]/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import { expect, suite, test } from 'vitest';
|
||||
import { toBase64 } from '../../../src/util/functions/toBase64';
|
||||
|
||||
suite('toBase64', () => {
|
||||
test('basic', () => {
|
||||
expect(atob(toBase64('duck'))).toBe('duck');
|
||||
});
|
||||
test('unicode', () => {
|
||||
expect(atob(toBase64('🦆'))).toBe('\xF0\x9F\xA6\x86');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user