Compare commits
49 Commits
Author | SHA1 | Date | |
---|---|---|---|
a6bb61764b | |||
7d867ea6ab | |||
d03f086c92 | |||
83c1e2bb4e | |||
faa5d6d542 | |||
6e06e8108b | |||
45ce23ec11 | |||
93ef6a3d6b | |||
609873b4ad | |||
4a8bbc3d13 | |||
51c2d4f6e0 | |||
45091d6b8c | |||
eae7300439 | |||
7753e58036 | |||
333e9c1316 | |||
479e0725e6 | |||
fbf177b482 | |||
3272b87e10 | |||
87c3548f91 | |||
af3d461593 | |||
bd9b3bb985 | |||
169e49b93d | |||
2db4010648 | |||
b4ae49b604 | |||
9bdd9484f1 | |||
1dcfbe7f5d | |||
e644fed03c | |||
b67acc1b78 | |||
51e76623f9 | |||
f792bd1907 | |||
0139634902 | |||
100cba8d74 | |||
2af84de938 | |||
f5ba97aa3d | |||
73529b353c | |||
88d1b6596f | |||
6ef8812a79 | |||
3034ef2edd | |||
4417c880b6 | |||
2f976ac5ce | |||
ff78573d16 | |||
aae91111e2 | |||
09e597ab87 | |||
d3ef78af11 | |||
4ea0dece4f | |||
ed550b7ff9 | |||
ab4ed2602f | |||
b8d929f4de | |||
cbc7b5a4a7 |
@ -18,17 +18,6 @@ before_script:
|
|||||||
# ====================
|
# ====================
|
||||||
# security stage
|
# security stage
|
||||||
# ====================
|
# ====================
|
||||||
mirror:
|
|
||||||
stage: security
|
|
||||||
script:
|
|
||||||
- npmci git mirror
|
|
||||||
only:
|
|
||||||
- tags
|
|
||||||
tags:
|
|
||||||
- lossless
|
|
||||||
- docker
|
|
||||||
- notpriv
|
|
||||||
|
|
||||||
auditProductionDependencies:
|
auditProductionDependencies:
|
||||||
image: registry.gitlab.com/hosttoday/ht-docker-node:npmci
|
image: registry.gitlab.com/hosttoday/ht-docker-node:npmci
|
||||||
stage: security
|
stage: security
|
||||||
@ -100,10 +89,9 @@ codequality:
|
|||||||
only:
|
only:
|
||||||
- tags
|
- tags
|
||||||
script:
|
script:
|
||||||
- npmci command npm install -g tslint typescript
|
- npmci command npm install -g typescript
|
||||||
- npmci npm prepare
|
- npmci npm prepare
|
||||||
- npmci npm install
|
- npmci npm install
|
||||||
- npmci command "tslint -c tslint.json ./ts/**/*.ts"
|
|
||||||
tags:
|
tags:
|
||||||
- lossless
|
- lossless
|
||||||
- docker
|
- docker
|
||||||
@ -123,11 +111,10 @@ trigger:
|
|||||||
pages:
|
pages:
|
||||||
stage: metadata
|
stage: metadata
|
||||||
script:
|
script:
|
||||||
- npmci node install lts
|
- npmci node install stable
|
||||||
- npmci command npm install -g @gitzone/tsdoc
|
|
||||||
- npmci npm prepare
|
- npmci npm prepare
|
||||||
- npmci npm install
|
- npmci npm install
|
||||||
- npmci command tsdoc
|
- npmci command npm run buildDocs
|
||||||
tags:
|
tags:
|
||||||
- lossless
|
- lossless
|
||||||
- docker
|
- docker
|
||||||
|
25262
package-lock.json
generated
25262
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
24
package.json
24
package.json
@ -1,27 +1,35 @@
|
|||||||
{
|
{
|
||||||
"name": "@pushrocks/smartclickhouse",
|
"name": "@pushrocks/smartclickhouse",
|
||||||
"version": "1.0.2",
|
"version": "2.0.14",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "an odm for talking to clickhouse",
|
"description": "an odm for talking to clickhouse",
|
||||||
"main": "dist_ts/index.js",
|
"main": "dist_ts/index.js",
|
||||||
"typings": "dist_ts/index.d.ts",
|
"typings": "dist_ts/index.d.ts",
|
||||||
|
"type": "module",
|
||||||
"author": "Lossless GmbH",
|
"author": "Lossless GmbH",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "(tstest test/ --web)",
|
"test": "(tstest test/ --web)",
|
||||||
"build": "(tsbuild --web)"
|
"build": "(tsbuild --web --allowimplicitany)",
|
||||||
|
"createGrafana": "docker run --name grafana -d -p 4000:3000 grafana/grafana-oss",
|
||||||
|
"createClickhouse": "docker run --name some-clickhouse-server --ulimit nofile=262144:262144 -p 8123:8123 -p 9000:9000 --volume=$PWD/.nogit/testdatabase:/var/lib/clickhouse yandex/clickhouse-server",
|
||||||
|
"buildDocs": "tsdoc"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@gitzone/tsbuild": "^2.1.25",
|
"@gitzone/tsbuild": "^2.1.65",
|
||||||
"@gitzone/tsbundle": "^1.0.78",
|
"@gitzone/tsbundle": "^2.0.7",
|
||||||
"@gitzone/tstest": "^1.0.44",
|
"@gitzone/tstest": "^1.0.73",
|
||||||
"@pushrocks/tapbundle": "^4.0.8",
|
"@pushrocks/tapbundle": "^5.0.4",
|
||||||
"@types/node": "^17.0.21",
|
"@types/node": "^18.6.4",
|
||||||
"tslint": "^6.1.3",
|
"tslint": "^6.1.3",
|
||||||
"tslint-config-prettier": "^1.15.0"
|
"tslint-config-prettier": "^1.15.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"clickhouse": "^2.4.4"
|
"@pushrocks/smartdelay": "^2.0.13",
|
||||||
|
"@pushrocks/smartobject": "^1.0.10",
|
||||||
|
"@pushrocks/smartpromise": "^3.1.7",
|
||||||
|
"@pushrocks/smarturl": "^3.0.2",
|
||||||
|
"@pushrocks/webrequest": "^3.0.12"
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
"last 1 chrome versions"
|
"last 1 chrome versions"
|
||||||
|
39
test/test.nonci.ts
Normal file
39
test/test.nonci.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { expect, expectAsync, tap } from '@pushrocks/tapbundle';
|
||||||
|
import * as smartclickhouse from '../ts/index.js';
|
||||||
|
|
||||||
|
let testClickhouseDb: smartclickhouse.SmartClickHouseDb;
|
||||||
|
|
||||||
|
tap.test('first test', async () => {
|
||||||
|
testClickhouseDb = new smartclickhouse.SmartClickHouseDb({
|
||||||
|
url: 'http://localhost:8123',
|
||||||
|
database: 'test2',
|
||||||
|
unref: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should start the clickhouse db', async () => {
|
||||||
|
await testClickhouseDb.start(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should create a timedatatable', async (toolsArg) => {
|
||||||
|
const table = await testClickhouseDb.getTable('analytics');
|
||||||
|
let i = 0;
|
||||||
|
while (i < 5000) {
|
||||||
|
await table.addData({
|
||||||
|
timestamp: Date.now(),
|
||||||
|
message: `hello this is a message ${i}`,
|
||||||
|
wow: 'hey',
|
||||||
|
deep: {
|
||||||
|
so: 'hello',
|
||||||
|
myArray: ['array1', 'array2'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
i++;
|
||||||
|
console.log(`logged ${i} of 5000 lines.`);
|
||||||
|
await toolsArg.delayFor(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.skip.test('should write something to the clickhouse db', async () => {});
|
||||||
|
|
||||||
|
tap.start();
|
30
test/test.ts
30
test/test.ts
@ -1,30 +0,0 @@
|
|||||||
import { expect, expectAsync, tap } from '@pushrocks/tapbundle';
|
|
||||||
import * as smartclickhouse from '../ts/index';
|
|
||||||
|
|
||||||
let testClickhouseDb: smartclickhouse.ClickhouseDb;
|
|
||||||
|
|
||||||
tap.test('first test', async () => {
|
|
||||||
testClickhouseDb = new smartclickhouse.ClickhouseDb({
|
|
||||||
url: 'http://localhost',
|
|
||||||
port: 8123,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('should start the clickhouse db', async () => {
|
|
||||||
await testClickhouseDb.start();
|
|
||||||
})
|
|
||||||
|
|
||||||
tap.test('should write something to the clickhouse db', async () => {
|
|
||||||
const result = await testClickhouseDb.clickhouseClient.query(`CREATE DATABASE IF NOT EXISTS lossless`);
|
|
||||||
console.log(result);
|
|
||||||
const result2 = await testClickhouseDb.clickhouseClient.query(`CREATE TABLE IF NOT EXISTS lossless.visits (
|
|
||||||
timestamp UInt64,
|
|
||||||
ip String,
|
|
||||||
os String,
|
|
||||||
userAgent String,
|
|
||||||
version String
|
|
||||||
)`);
|
|
||||||
console.log(result);
|
|
||||||
})
|
|
||||||
|
|
||||||
tap.start();
|
|
8
ts/00_commitinfo_data.ts
Normal file
8
ts/00_commitinfo_data.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* autocreated commitinfo by @pushrocks/commitinfo
|
||||||
|
*/
|
||||||
|
export const commitinfo = {
|
||||||
|
name: '@pushrocks/smartclickhouse',
|
||||||
|
version: '2.0.14',
|
||||||
|
description: 'an odm for talking to clickhouse'
|
||||||
|
}
|
24
ts/index.ts
24
ts/index.ts
@ -1,22 +1,2 @@
|
|||||||
import * as plugins from './smartclickhouse.plugins';
|
export * from './smartclickhouse.classes.smartclickhouse.js';
|
||||||
|
export * from './smartclickhouse.classes.timedatatable.js';
|
||||||
export interface IClickhouseConstructorOptions {
|
|
||||||
url: string;
|
|
||||||
port?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ClickhouseDb {
|
|
||||||
public options: IClickhouseConstructorOptions;
|
|
||||||
public clickhouseClient: plugins.clickhouse.ClickHouse;
|
|
||||||
|
|
||||||
constructor(optionsArg: IClickhouseConstructorOptions) {
|
|
||||||
this.options = optionsArg;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* starts the connection to the Clickhouse db
|
|
||||||
*/
|
|
||||||
public start() {
|
|
||||||
this.clickhouseClient = new plugins.clickhouse.ClickHouse(this.options);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
3
ts/smartclickhouse.classes.clickhousedb.ts
Normal file
3
ts/smartclickhouse.classes.clickhousedb.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import * as plugins from './smartclickhouse.plugins.js';
|
||||||
|
|
||||||
|
export class ClickhouseDb {}
|
100
ts/smartclickhouse.classes.httpclient.ts
Normal file
100
ts/smartclickhouse.classes.httpclient.ts
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import * as plugins from './smartclickhouse.plugins.js';
|
||||||
|
|
||||||
|
export interface IClickhouseHttpClientOptions {
|
||||||
|
username?: string;
|
||||||
|
password?: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ClickhouseHttpClient {
|
||||||
|
// STATIC
|
||||||
|
public static async createAndStart(optionsArg: IClickhouseHttpClientOptions) {
|
||||||
|
const clickhouseHttpInstance = new ClickhouseHttpClient(optionsArg);
|
||||||
|
await clickhouseHttpInstance.start();
|
||||||
|
return clickhouseHttpInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// INSTANCE
|
||||||
|
public options: IClickhouseHttpClientOptions;
|
||||||
|
public webrequestInstance = new plugins.webrequest.WebRequest({
|
||||||
|
logging: false,
|
||||||
|
});
|
||||||
|
public computedProperties: {
|
||||||
|
connectionUrl: string;
|
||||||
|
parsedUrl: plugins.smarturl.Smarturl;
|
||||||
|
} = {
|
||||||
|
connectionUrl: null,
|
||||||
|
parsedUrl: null,
|
||||||
|
};
|
||||||
|
constructor(optionsArg: IClickhouseHttpClientOptions) {
|
||||||
|
this.options = optionsArg;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async start() {
|
||||||
|
this.computedProperties.parsedUrl = plugins.smarturl.Smarturl.createFromUrl(this.options.url);
|
||||||
|
console.log(this.computedProperties.parsedUrl);
|
||||||
|
this.computedProperties.connectionUrl = this.computedProperties.parsedUrl.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ping() {
|
||||||
|
const ping = await this.webrequestInstance.request(
|
||||||
|
this.computedProperties.connectionUrl.toString(),
|
||||||
|
{
|
||||||
|
method: 'GET',
|
||||||
|
timeoutMs: 1000,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return ping.status === 200 ? true : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async queryPromise(queryArg: string) {
|
||||||
|
const returnArray = [];
|
||||||
|
const response = await this.webrequestInstance.request(
|
||||||
|
`${this.computedProperties.connectionUrl}?query=${encodeURIComponent(queryArg)}`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: this.getHeaders(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
// console.log('===================');
|
||||||
|
// console.log(this.computedProperties.connectionUrl);
|
||||||
|
// console.log(queryArg);
|
||||||
|
// console.log((await response.clone().text()).split(/\r?\n/))
|
||||||
|
if (response.headers.get('X-ClickHouse-Format') === 'JSONEachRow') {
|
||||||
|
const jsonList = await response.text();
|
||||||
|
const jsonArray = jsonList.split('\n');
|
||||||
|
for (const jsonArg of jsonArray) {
|
||||||
|
if (!jsonArg) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
returnArray.push(JSON.parse(jsonArg));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
}
|
||||||
|
return returnArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async insertPromise(databaseArg: string, tableArg: string, documents: any[]) {
|
||||||
|
const queryArg = `INSERT INTO ${databaseArg}.${tableArg} FORMAT JSONEachRow`;
|
||||||
|
const response = await this.webrequestInstance.request(
|
||||||
|
`${this.computedProperties.connectionUrl}?query=${encodeURIComponent(queryArg)}`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
body: documents.map((docArg) => JSON.stringify(docArg)).join('\n'),
|
||||||
|
headers: this.getHeaders(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getHeaders() {
|
||||||
|
const headers: { [key: string]: string } = {};
|
||||||
|
if (this.options.username) {
|
||||||
|
headers['X-ClickHouse-User'] = this.options.username;
|
||||||
|
}
|
||||||
|
if (this.options.password) {
|
||||||
|
headers['X-ClickHouse-Key'] = this.options.password;
|
||||||
|
}
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
}
|
67
ts/smartclickhouse.classes.smartclickhouse.ts
Normal file
67
ts/smartclickhouse.classes.smartclickhouse.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import * as plugins from './smartclickhouse.plugins.js';
|
||||||
|
import { TimeDataTable } from './smartclickhouse.classes.timedatatable.js';
|
||||||
|
import { ClickhouseHttpClient } from './smartclickhouse.classes.httpclient.js';
|
||||||
|
|
||||||
|
export interface IClickhouseConstructorOptions {
|
||||||
|
url: string;
|
||||||
|
database: string;
|
||||||
|
username?: string;
|
||||||
|
password?: string;
|
||||||
|
/**
|
||||||
|
* allow services to exit when waiting for clickhouse startup
|
||||||
|
* this allows to leave the lifecycle flow to other processes
|
||||||
|
* like a listening server.
|
||||||
|
*/
|
||||||
|
unref?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SmartClickHouseDb {
|
||||||
|
public options: IClickhouseConstructorOptions;
|
||||||
|
public clickhouseClient: ClickhouseHttpClient;
|
||||||
|
|
||||||
|
constructor(optionsArg: IClickhouseConstructorOptions) {
|
||||||
|
this.options = optionsArg;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* starts the connection to the Clickhouse db
|
||||||
|
*/
|
||||||
|
public async start(dropOld = false) {
|
||||||
|
console.log(`Connecting to default database first.`);
|
||||||
|
// lets connect
|
||||||
|
this.clickhouseClient = await ClickhouseHttpClient.createAndStart(this.options);
|
||||||
|
await this.pingDatabaseUntilAvailable();
|
||||||
|
console.log(`Create database ${this.options.database}, if it does not exist...`);
|
||||||
|
await this.createDatabase(dropOld);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async createDatabase(dropOld: boolean = false) {
|
||||||
|
if (dropOld) {
|
||||||
|
await this.clickhouseClient.queryPromise(`DROP DATABASE IF EXISTS ${this.options.database}`);
|
||||||
|
}
|
||||||
|
await this.clickhouseClient.queryPromise(
|
||||||
|
`CREATE DATABASE IF NOT EXISTS ${this.options.database}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async pingDatabaseUntilAvailable() {
|
||||||
|
let available = false;
|
||||||
|
while (!available) {
|
||||||
|
available = await this.clickhouseClient.ping().catch((err) => {
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
if (!available) {
|
||||||
|
console.log(`NOT OK: tried pinging ${this.options.url}... Trying again in 5 seconds.`);
|
||||||
|
await plugins.smartdelay.delayFor(5000, null, this.options.unref);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* gets a table
|
||||||
|
*/
|
||||||
|
public async getTable(tableName: string) {
|
||||||
|
const newTable = TimeDataTable.getTable(this, tableName);
|
||||||
|
return newTable;
|
||||||
|
}
|
||||||
|
}
|
200
ts/smartclickhouse.classes.timedatatable.ts
Normal file
200
ts/smartclickhouse.classes.timedatatable.ts
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
import * as plugins from './smartclickhouse.plugins.js';
|
||||||
|
import { SmartClickHouseDb } from './smartclickhouse.classes.smartclickhouse.js';
|
||||||
|
|
||||||
|
export type TClickhouseColumnDataType =
|
||||||
|
| 'String'
|
||||||
|
| "DateTime64(3, 'Europe/Berlin')"
|
||||||
|
| 'Float64'
|
||||||
|
| 'Array(String)'
|
||||||
|
| 'Array(Float64)';
|
||||||
|
export interface IColumnInfo {
|
||||||
|
database: string;
|
||||||
|
table: string;
|
||||||
|
name: string;
|
||||||
|
type: TClickhouseColumnDataType;
|
||||||
|
position: string;
|
||||||
|
default_kind: string;
|
||||||
|
default_expression: string;
|
||||||
|
data_compressed_bytes: string;
|
||||||
|
data_uncompressed_bytes: string;
|
||||||
|
marks_bytes: string;
|
||||||
|
comment: string;
|
||||||
|
is_in_partition_key: 0 | 1;
|
||||||
|
is_in_sorting_key: 0 | 1;
|
||||||
|
is_in_primary_key: 0 | 1;
|
||||||
|
is_in_sampling_key: 0 | 1;
|
||||||
|
compression_codec: string;
|
||||||
|
character_octet_length: null;
|
||||||
|
numeric_precision: null;
|
||||||
|
numeric_precision_radix: null;
|
||||||
|
numeric_scale: null;
|
||||||
|
datetime_precision: '3';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ITimeDataTableOptions {
|
||||||
|
tableName: string;
|
||||||
|
retainDataForDays: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TimeDataTable {
|
||||||
|
public static async getTable(smartClickHouseDbRefArg: SmartClickHouseDb, tableNameArg: string) {
|
||||||
|
const newTable = new TimeDataTable(smartClickHouseDbRefArg, {
|
||||||
|
tableName: tableNameArg,
|
||||||
|
retainDataForDays: 30,
|
||||||
|
});
|
||||||
|
|
||||||
|
await newTable.setup();
|
||||||
|
|
||||||
|
return newTable;
|
||||||
|
}
|
||||||
|
|
||||||
|
// INSTANCE
|
||||||
|
public healingDeferred: plugins.smartpromise.Deferred<any>;
|
||||||
|
public smartClickHouseDbRef: SmartClickHouseDb;
|
||||||
|
public options: ITimeDataTableOptions;
|
||||||
|
|
||||||
|
constructor(smartClickHouseDbRefArg: SmartClickHouseDb, optionsArg: ITimeDataTableOptions) {
|
||||||
|
this.smartClickHouseDbRef = smartClickHouseDbRefArg;
|
||||||
|
this.options = optionsArg;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async setup() {
|
||||||
|
// create table in clickhouse
|
||||||
|
await this.smartClickHouseDbRef.clickhouseClient.queryPromise(`
|
||||||
|
CREATE TABLE IF NOT EXISTS ${this.smartClickHouseDbRef.options.database}.${this.options.tableName} (
|
||||||
|
timestamp DateTime64(3, 'Europe/Berlin'),
|
||||||
|
message String
|
||||||
|
) ENGINE=MergeTree() ORDER BY timestamp`);
|
||||||
|
|
||||||
|
// lets adjust the TTL
|
||||||
|
await this.smartClickHouseDbRef.clickhouseClient.queryPromise(`
|
||||||
|
ALTER TABLE ${this.smartClickHouseDbRef.options.database}.${this.options.tableName} MODIFY TTL toDateTime(timestamp) + INTERVAL ${this.options.retainDataForDays} DAY
|
||||||
|
`);
|
||||||
|
|
||||||
|
await this.updateColumns();
|
||||||
|
console.log(`=======================`);
|
||||||
|
console.log(
|
||||||
|
`table with name "${this.options.tableName}" in database ${this.smartClickHouseDbRef.options.database} has the following columns:`
|
||||||
|
);
|
||||||
|
for (const column of this.columns) {
|
||||||
|
console.log(`>> ${column.name}: ${column.type}`);
|
||||||
|
}
|
||||||
|
console.log('^^^^^^^^^^^^^^\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
public columns: IColumnInfo[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* updates the columns
|
||||||
|
*/
|
||||||
|
public async updateColumns() {
|
||||||
|
this.columns = await this.smartClickHouseDbRef.clickhouseClient.queryPromise(`
|
||||||
|
SELECT * FROM system.columns
|
||||||
|
WHERE database LIKE '${this.smartClickHouseDbRef.options.database}'
|
||||||
|
AND table LIKE '${this.options.tableName}' FORMAT JSONEachRow
|
||||||
|
`);
|
||||||
|
return this.columns;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* stores a json and tries to map it to the nested syntax
|
||||||
|
*/
|
||||||
|
public async addData(dataArg: any) {
|
||||||
|
if (this.healingDeferred) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// the storageJson
|
||||||
|
let storageJson: { [key: string]: any } = {};
|
||||||
|
|
||||||
|
// helper stuff
|
||||||
|
|
||||||
|
const getClickhouseTypeForValue = (valueArg: any): TClickhouseColumnDataType => {
|
||||||
|
const typeConversion: { [key: string]: TClickhouseColumnDataType } = {
|
||||||
|
string: 'String',
|
||||||
|
number: 'Float64',
|
||||||
|
undefined: null,
|
||||||
|
null: null,
|
||||||
|
};
|
||||||
|
if (valueArg instanceof Array) {
|
||||||
|
const arrayType = typeConversion[typeof valueArg[0] as string];
|
||||||
|
if (!arrayType) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return `Array(${arrayType})` as TClickhouseColumnDataType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return typeConversion[typeof valueArg as string];
|
||||||
|
};
|
||||||
|
const checkPath = async (
|
||||||
|
pathArg: string,
|
||||||
|
typeArg: TClickhouseColumnDataType,
|
||||||
|
prechecked = false
|
||||||
|
) => {
|
||||||
|
let columnFound = false;
|
||||||
|
for (const column of this.columns) {
|
||||||
|
if (pathArg === column.name) {
|
||||||
|
columnFound = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!columnFound) {
|
||||||
|
if (!prechecked) {
|
||||||
|
await this.updateColumns();
|
||||||
|
await checkPath(pathArg, typeArg, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const alterString = `ALTER TABLE ${this.smartClickHouseDbRef.options.database}.${this.options.tableName} ADD COLUMN ${pathArg} ${typeArg} FIRST`;
|
||||||
|
try {
|
||||||
|
await this.smartClickHouseDbRef.clickhouseClient.queryPromise(`
|
||||||
|
${alterString}
|
||||||
|
`);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(alterString);
|
||||||
|
for (const column of this.columns) {
|
||||||
|
console.log(column.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await this.updateColumns();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// key checking
|
||||||
|
const flatDataArg = plugins.smartobject.toFlatObject(dataArg);
|
||||||
|
for (const key of Object.keys(flatDataArg)) {
|
||||||
|
const value = flatDataArg[key];
|
||||||
|
if (key === 'timestamp' && typeof value !== 'number') {
|
||||||
|
throw new Error('timestamp must be of type number');
|
||||||
|
} else if (key === 'timestamp') {
|
||||||
|
storageJson.timestamp = flatDataArg[key];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// lets deal with the rest
|
||||||
|
const clickhouseType = getClickhouseTypeForValue(value);
|
||||||
|
if (!clickhouseType) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
await checkPath(key, clickhouseType);
|
||||||
|
storageJson[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await this.smartClickHouseDbRef.clickhouseClient
|
||||||
|
.insertPromise(this.smartClickHouseDbRef.options.database, this.options.tableName, [
|
||||||
|
storageJson,
|
||||||
|
])
|
||||||
|
.catch(async () => {
|
||||||
|
if (this.healingDeferred) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.healingDeferred = plugins.smartpromise.defer();
|
||||||
|
console.log(`Ran into an error. Trying to set up things properly again.`);
|
||||||
|
await this.smartClickHouseDbRef.pingDatabaseUntilAvailable();
|
||||||
|
await this.smartClickHouseDbRef.createDatabase();
|
||||||
|
await this.setup();
|
||||||
|
this.columns = [];
|
||||||
|
this.healingDeferred.resolve();
|
||||||
|
this.healingDeferred = null;
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,8 @@
|
|||||||
import * as clickhouse from 'clickhouse';
|
// @pushrocks scope
|
||||||
|
import * as smartdelay from '@pushrocks/smartdelay';
|
||||||
|
import * as smartobject from '@pushrocks/smartobject';
|
||||||
|
import * as smartpromise from '@pushrocks/smartpromise';
|
||||||
|
import * as smarturl from '@pushrocks/smarturl';
|
||||||
|
import * as webrequest from '@pushrocks/webrequest';
|
||||||
|
|
||||||
export {
|
export { smartdelay, smartobject, smartpromise, smarturl, webrequest };
|
||||||
clickhouse
|
|
||||||
}
|
|
||||||
|
10
tsconfig.json
Normal file
10
tsconfig.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"useDefineForClassFields": false,
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "ES2022",
|
||||||
|
"moduleResolution": "nodenext",
|
||||||
|
"esModuleInterop": true
|
||||||
|
}
|
||||||
|
}
|
17
tslint.json
17
tslint.json
@ -1,17 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": ["tslint:latest", "tslint-config-prettier"],
|
|
||||||
"rules": {
|
|
||||||
"semicolon": [true, "always"],
|
|
||||||
"no-console": false,
|
|
||||||
"ordered-imports": false,
|
|
||||||
"object-literal-sort-keys": false,
|
|
||||||
"member-ordering": {
|
|
||||||
"options":{
|
|
||||||
"order": [
|
|
||||||
"static-method"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"defaultSeverity": "warning"
|
|
||||||
}
|
|
Reference in New Issue
Block a user