From 7fbbefbbbe60e49b8af13f0f689fa79ec5fcdaa6 Mon Sep 17 00:00:00 2001 From: Sayan Nandan Date: Thu, 28 Dec 2023 19:43:05 +0530 Subject: [PATCH 1/4] Add query impls --- .gitignore | 3 +- src/config.ts | 121 ----------------- src/connection.ts | 54 -------- src/index.ts | 5 +- src/protocol.ts | 326 ++++++++++++---------------------------------- src/query.ts | 120 ++++++++++++----- src/skytable.ts | 41 ------ src/utils.ts | 3 + 8 files changed, 178 insertions(+), 495 deletions(-) delete mode 100644 src/config.ts delete mode 100644 src/connection.ts delete mode 100644 src/skytable.ts create mode 100644 src/utils.ts diff --git a/.gitignore b/.gitignore index 1f4706a..311aefb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ node_modules .devcontainer dist -.vscode \ No newline at end of file +.vscode +.rollup_cache \ No newline at end of file diff --git a/src/config.ts b/src/config.ts deleted file mode 100644 index a89b834..0000000 --- a/src/config.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { - connectionWrite, - createConnection, - createConnectionTls, -} from './connection'; -import { createDB } from './skytable'; -import { bufferToHandshakeResult, getClientHandshake } from './protocol'; -import type { - ConnectionOptions as ConnectionTLSOptions, - TLSSocket, -} from 'node:tls'; -import { Socket } from 'node:net'; - -/** - * Configuration for a client connection (single node) - */ -export class Config { - private username: string; - private password: string; - private host: string; - private port: number; - private connection: Socket | TLSSocket | undefined; - - /** - * Create a new configuration - * - * @param username Set the username for authenticating with Skytable - * @param password Set the password for authenticating with Skytable - * @param host Set the host to connect to Skytable (defaults to `127.0.0.1`) - * @param port Set the port to connect to Skytable (defaults to 2003) - */ - constructor( - username: string, - password: string, - host: string = 'localhost', - port: number = 2003, - ) { - this.username = username; - this.password = password; - this.host = host; - this.port = port; - } - - /** - * Get the set username - * @returns Username set in this configuration - */ - getUsername(): string { - return this.username; - } - - /** - * Get the set password - * @returns Username set in this configuration - */ - getPassword(): string { - return this.password; - } - - /** - * Get the set host - * @returns Username set in this configuration - */ - getHost(): string { - return this.host; - } - - /** - * Get the set port - * @returns Username set in this configuration - */ - getPort(): number { - return this.port; - } - - /** - * Get the connection - * @returns The current connection - */ - getConnection() { - return this.connection; - } - - /** - * connect to Skytable - */ - async connect() { - const socket = await createConnection({ - port: this.port, - host: this.host, - }); - const data = await connectionWrite(socket, getClientHandshake(this)); - await bufferToHandshakeResult(data); - this.connection = socket; - return createDB(socket); - } - - /** - * connect to Skytable - */ - async connectTLS(options: ConnectionTLSOptions) { - const socket = await createConnectionTls({ - port: this.port, - host: this.host, - ...options, - }); - const data = await connectionWrite(socket, getClientHandshake(this)); - await bufferToHandshakeResult(data); - this.connection = socket; - return createDB(socket); - } - - /** - * disconnect from Skytable - */ - async disconnect() { - if (this.connection) { - this.connection.destroySoon(); - } - } -} diff --git a/src/connection.ts b/src/connection.ts deleted file mode 100644 index 157ef8b..0000000 --- a/src/connection.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { connect as connectTcp, Socket, NetConnectOpts } from 'node:net'; -import { - connect as connectTcpTLS, - TLSSocket, - ConnectionOptions as ConnectionTLSOptions, -} from 'node:tls'; - -export function createConnection(options: NetConnectOpts): Promise { - return new Promise((resolve, reject) => { - const conn = connectTcp(options); - conn.once('connect', () => { - resolve(conn); - }); - conn.once('error', (error) => { - console.error(`createConnection error: ${error.message}`); - reject(error); - }); - }); -} - -export function createConnectionTls( - options: ConnectionTLSOptions, -): Promise { - return new Promise((resolve, reject) => { - const conn = connectTcpTLS(options); - conn.once('connect', () => { - resolve(conn); - }); - conn.once('error', (error) => { - console.error(`createConnection error: ${error.message}`); - reject(error); - }); - }); -} - -export function connectionWrite( - connect: Socket | TLSSocket, - buffer: Buffer | string, -): Promise { - return new Promise((resolve, reject) => { - connect.write(buffer, (writeError) => { - if (writeError) { - reject(writeError); - return; - } - connect.once('data', (data) => { - resolve(data); - }); - connect.once('error', (err) => { - reject(err); - }); - }); - }); -} diff --git a/src/index.ts b/src/index.ts index 38d07c1..2a15f56 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1 @@ -export { Config as default } from './config'; -export * from './skytable'; -export * from './protocol'; -export * from './query'; +export { Query, Parameter, SignedInt } from './query'; diff --git a/src/protocol.ts b/src/protocol.ts index a89656f..01931aa 100644 --- a/src/protocol.ts +++ b/src/protocol.ts @@ -1,257 +1,93 @@ -import { Config } from './config'; -import type { Column, QueryResult, Row, Rows, SQParam } from './skytable'; -import { Query } from './query'; - -const PARAMS_TYPE = { - NULL: '\x00', - BOOLEAN: '\x01', - UINT: '\x02', - SINT: '\x03', - FLOAT: '\x04', - BINARY: '\x05', - STRING: '\x06', -}; - -const RESPONSES_RESULT = { - NULL: 0, - BOOL: 1, - U8INT: 2, - U16INT: 3, - U32INT: 4, - U64INT: 5, - S8INT: 6, - S16INT: 7, - S32INT: 8, - S64INT: 9, - FLOAT32: 10, - FLOAT64: 11, - BINARY: 12, - STRING: 13, - LIST: 14, - ERROR: 0x10, - ROW: 0x11, - EMPTY: 0x12, - MULTIROW: 0x13, +import { Parameter, Query, isSQParam } from './query'; +import { isFloat } from './utils'; + +export const PARAMS_TYPE = { + NULL: new Uint8Array([0x00]), + BOOLEAN: new Uint8Array([0x01]), + UINT: new Uint8Array([0x02]), + SINT: new Uint8Array([0x03]), + FLOAT: new Uint8Array([0x04]), + BINARY: new Uint8Array([0x05]), + STRING: new Uint8Array([0x06]), }; -const HANDSHAKE_RESULT = { - SUCCESS: 'H00', - ERROR: 'H01', -}; - -function isFloat(number: number | string): boolean { - return Number.isFinite(number) && !Number.isInteger(number); -} +export const NEWLINE = new Uint8Array([0x0A]); +const STATICALLY_ENCODED_BOOL_FALSE = new Uint8Array([0x00]); +const STATICALLY_ENCODED_BOOL_TRUE = new Uint8Array([0x01]); -export function encodeParam(param: SQParam): string { - // 5 A binary blob [5\n] - if (Buffer.isBuffer(param)) { - return [PARAMS_TYPE.BINARY, param.length, '\n', param.toString()].join(''); - } - // null undefined - if (param == null) { - return '\x00'; - } +export function encodeParams(query: Query, param: Parameter): void { switch (typeof param) { - case 'string': - return [PARAMS_TYPE.STRING, param.length, '\n', param].join(''); - case 'number': - // 2 Unsigned integer 64 - // 3 Signed integer 64 - // 4 Float A 64-bit - return [ - isFloat(param) - ? PARAMS_TYPE.FLOAT - : param < 0 - ? PARAMS_TYPE.SINT - : PARAMS_TYPE.UINT, - String(param), - '\n', - ].join(''); - case 'bigint': - return [ - param < 0 ? PARAMS_TYPE.SINT : PARAMS_TYPE.UINT, - String(param), - '\n', - ].join(''); - case 'boolean': - return [PARAMS_TYPE.BOOLEAN, Number(param) === 1 ? '\x01' : 0].join(''); - default: - throw new TypeError(`un support type: ${typeof param}, val: ${param}`); - } -} - -export function encodeParams(parameters: SQParam[]): string { - return parameters.map(encodeParam).join(''); -} - -export function encodeQuery(query: Query): Buffer { - const dataframe = `${query.getQuery()}${query.getParams().join('')}`; - const data = [query.getQueryLength(), '\n', dataframe]; - const requestData = ['S', data.join('').length, '\n', ...data]; - return Buffer.from(requestData.join(''), 'utf-8'); -} - -function getFirstSplitOffset(buffer: Buffer, split = '\n'): number { - for (let i = 0; i < buffer.length; i++) { - if (buffer[i] === split.charCodeAt(0)) { - return i; + case 'boolean': { + query + .getParamBuffer() + .push( + Buffer.concat([ + PARAMS_TYPE.BOOLEAN, + param + ? STATICALLY_ENCODED_BOOL_TRUE + : STATICALLY_ENCODED_BOOL_FALSE, + ]), + ); + break; } - } - return -1; -} - -function parseNumber( - formatFn: (string: string) => T, - buffer: Buffer, -): [T, Buffer] { - const offset = getFirstSplitOffset(buffer); - const val = formatFn(buffer.subarray(0, offset).toString('utf-8')); - return [val, buffer.subarray(offset + 1)]; -} - -function parseNextBySize(size: number, buffer: Buffer): [Column[], Buffer] { - let values = []; - let nextBuffer = buffer; - for (let i = 0; i < size; i++) { - const [value, remainingBuffer] = decodeValue(nextBuffer); - values.push(value); - nextBuffer = remainingBuffer; - } - return [values, nextBuffer]; -} - -function decodeValue(buffer: Buffer): [Column, Buffer] { - const type = buffer.readUInt8(0); - buffer = buffer.subarray(1); - switch (type) { - case RESPONSES_RESULT.NULL: // Null - return [null, buffer.subarray(0)]; - case RESPONSES_RESULT.BOOL: // Bool - return [Boolean(buffer.readUInt8(0)), buffer.subarray(1)]; - case RESPONSES_RESULT.U8INT: // 8-bit Unsigned Integer - return parseNumber(Number, buffer); - case RESPONSES_RESULT.U16INT: // 16-bit Unsigned Integer - return parseNumber(Number, buffer); - case RESPONSES_RESULT.U32INT: // 32-bit Unsigned Integer - return parseNumber(Number, buffer); - case RESPONSES_RESULT.U64INT: // 64-bit Unsigned Integer - return parseNumber(BigInt, buffer); - case RESPONSES_RESULT.S8INT: // 8-bit Signed Integer - return parseNumber(Number, buffer); - case RESPONSES_RESULT.S16INT: // 16-bit Signed Integer - return parseNumber(Number, buffer); - case RESPONSES_RESULT.S32INT: // 32-bit Signed Integer - return parseNumber(Number, buffer); - case RESPONSES_RESULT.S64INT: // 64-bit Signed Integer - return parseNumber(BigInt, buffer); - case RESPONSES_RESULT.FLOAT32: // f32 - return parseNumber(Number.parseFloat, buffer); - case RESPONSES_RESULT.FLOAT64: // f64 - return parseNumber(Number.parseFloat, buffer); - case RESPONSES_RESULT.BINARY: { - // Binary \n, - const sizeOffset = getFirstSplitOffset(buffer); - const size = Number(buffer.subarray(0, sizeOffset).toString('utf-8')); - if (size === 0) { - return [Buffer.from([]), buffer.subarray(sizeOffset + 1)]; - } - const [start, end] = [sizeOffset + 1, sizeOffset + 1 + Number(size)]; - return [buffer.subarray(start, end), buffer.subarray(end)]; + case 'number': { + query + .getParamBuffer() + .push( + Buffer.concat([ + isFloat(param) + ? PARAMS_TYPE.FLOAT + : param < 0 + ? PARAMS_TYPE.SINT + : PARAMS_TYPE.UINT, + Buffer.from(param.toString()), + NEWLINE, + ]), + ); + break; } - case RESPONSES_RESULT.STRING: { - // String \n - const sizeOffset = getFirstSplitOffset(buffer); - const size = Number(buffer.subarray(0, sizeOffset).toString('utf-8')); - const [start, end] = [sizeOffset + 1, sizeOffset + 1 + Number(size)]; - const str = buffer.subarray(start, end).toString('utf-8'); - return [str, buffer.subarray(end)]; + case 'bigint': { + query + .getParamBuffer() + .push( + Buffer.concat([ + param < 0 ? PARAMS_TYPE.SINT : PARAMS_TYPE.UINT, + Buffer.from(param.toString()), + NEWLINE, + ]), + ); + break; } - case RESPONSES_RESULT.LIST: { - // List \n - const sizeOffset = getFirstSplitOffset(buffer); - const size = Number(buffer.subarray(0, sizeOffset).toString('utf-8')); - if (size === 0) { - return [[], buffer.subarray(sizeOffset + 1)]; + case 'string': { + query + .getParamBuffer() + .push(Buffer.concat([PARAMS_TYPE.STRING, Buffer.from(param), NEWLINE])); + break; + } + case 'object': { + if (param === null) { + query.getParamBuffer().push(PARAMS_TYPE.NULL); + break; + } else if (param instanceof Buffer) { + query + .getParamBuffer() + .push( + Buffer.concat([ + PARAMS_TYPE.BINARY, + Buffer.from(param.length.toString()), + NEWLINE, + param, + ]), + ); + break; + } else if (isSQParam(param)) { + return query.incrQueryCountBy( + param.encodeUnsafe(query.getParamBuffer()), + ); } - return parseNextBySize(size, buffer.subarray(sizeOffset + 1)) as [ - Column, - Buffer, - ]; } default: - throw new Error(`Unknown data type: ${type}`); - } -} - -export function decodeRow(buffer: Buffer): Row { - const offset = getFirstSplitOffset(buffer); - const columnCount = Number(buffer.subarray(0, offset).toString('utf-8')); - const dataType = buffer.subarray(offset + 1); - const [row] = parseNextBySize(columnCount, dataType); - return row; -} - -export function decodeRows(buffer: Buffer): Rows { - const offset = getFirstSplitOffset(buffer); - const rowCount = Number(buffer.subarray(0, offset).toString('utf-8')); - buffer = buffer.subarray(offset + 1); - const columnOffset = getFirstSplitOffset(buffer); - const columnCount = Number( - buffer.subarray(0, columnOffset).toString('utf-8'), - ); - buffer = buffer.subarray(columnOffset + 1); - const result: Rows = []; - let nextBuffer = buffer; - for (let i = 0; i < rowCount; i++) { - const [row, remainingBuffer] = parseNextBySize(columnCount, nextBuffer); - result[i] = row; - nextBuffer = remainingBuffer; - } - return result; -} - -export function decodeResponse(buffer: Buffer): QueryResult { - const type = buffer.readInt8(0); - switch (type) { - case RESPONSES_RESULT.EMPTY: - return null; - case RESPONSES_RESULT.ROW: - return decodeRow(buffer.subarray(1)); - case RESPONSES_RESULT.MULTIROW: - return decodeRows(buffer.subarray(1)); - case RESPONSES_RESULT.ERROR: - throw new Error( - `response error code: ${buffer.subarray(1, 2).readInt8()}`, - ); - default: - break; + throw new TypeError(`unsupported type: ${typeof param}, val: ${param}`); } - const [val] = decodeValue(buffer); - return val; -} - -export function getClientHandshake(config: Config): string { - const username = config.getUsername(); - const password = config.getPassword(); - return [ - 'H\x00\x00\x00\x00\x00', - username.length, - '\n', - password.length, - '\n', - username, - password, - ].join(''); -} - -export function bufferToHandshakeResult(buffer: Buffer): Promise { - return new Promise((resolve, reject) => { - const [h, c1, c2, msg] = Array.from(buffer.toJSON().data); - const code = [String.fromCharCode(h), c1, c2].join(''); - if (code === HANDSHAKE_RESULT.SUCCESS) { - return resolve(); - } - reject(new Error(`handshake error code ${code}, msg: ${msg}`)); - }); + query.incrQueryCountBy(1); } diff --git a/src/query.ts b/src/query.ts index 005cbde..6c64c68 100644 --- a/src/query.ts +++ b/src/query.ts @@ -1,58 +1,120 @@ -import { encodeParam } from './protocol'; -import { SQParam } from './skytable'; +import { encodeParams as encodeParam, NEWLINE, PARAMS_TYPE } from './protocol'; +import { isFloat } from './utils'; +/** + * A type that can be accepted as a parameter + */ +export type Parameter = + | null + | boolean + | bigint + | number + | Buffer + | string + | SQParam; + +export function isSQParam(obj: any): obj is SQParam { + return (obj as SQParam).encodeUnsafe != undefined; +} + +/** + * A query object represents a query that is to be sent to the + */ export class Query { private query: string; - private params: string[]; - + private params: Uint8Array[]; + private param_cnt: number; /** - * Create a new query with the base query set. You can now add (any) additional parameters. + * Create a new query * - * @param query The base query + * @param query base query + * @param params additional parameters */ - constructor(query: string) { + constructor(query: string | string, ...params: Parameter[]) { this.query = query; + this.param_cnt = 0; this.params = []; + params.forEach((param) => { + encodeParam(this, param); + }); } /** - * Get the query string - * @returns query string + * Get the base query payload + * @returns base query */ - getQuery(): string { + public getQuery(): String { return this.query; } - /** - * Get the Params - * @returns params encoded string + * Get parameter count + * @returns parameter count */ - getParams(): string[] { - return this.params; + public getParamCount(): number { + return this.params.length; } /** - * Get the number of parameters - * - * @returns Returns a count of the number of parameters + * Add a new parameter to this query + * @param param new parameter */ - public getParamCount(): number { - return this.params.length; + public pushParam(param: Parameter): void { + encodeParam(this, param); } - + // private /** - * Get the query length * - * @returns Returns the length of the query + * @returns returns the parameter buffer */ - public getQueryLength(): number { - return this.query.length; + getParamBuffer(): Uint8Array[] { + return this.params; } /** + * Increment the parameter count by the given value * - * Add a new parameter to your query + * @param by newly added param count + */ + incrQueryCountBy(by: number): void { + this.param_cnt += by; + } +} + +/** + * If you want to use a custom parameter, implement this interface + */ +export interface SQParam { + /** + * Encode the parameter buffer and return the number of parameters that were added. If you were encoding an + * object, you would return the number of keys that you encoded, for example. * - * @param param Query input parameter + * @param buf The raw parameter buffer */ - public pushParam(param: SQParam): void { - this.params.push(encodeParam(param)); + encodeUnsafe(buf: Uint8Array[]): number; +} + +/** + * The `SignedInt` class is meant to be used when you're trying to pass an unsigned integer into a query as a parameter. We always + * recommend doing this because the @method Query.pushParam simply checks if the value is positive or negative and then encodes it + * as either an unsigned int or a signed int, respectively. + * + * Hence, for positive values that you intended to be sent as a signed integer (as is stored in the database), use this. + * + * > TL;DR: use this to push parameters for signed integer columns + */ +export class SignedInt implements SQParam { + public value: number; + constructor(value: number) { + if (isFloat(value)) { + throw new TypeError('expected a non-floating point value'); + } + this.value = value; + } + encodeUnsafe(buf: Uint8Array[]): number { + buf.push( + Buffer.concat([ + PARAMS_TYPE.SINT, + Buffer.from(this.value.toString()), + NEWLINE, + ]), + ); + return 1; } } diff --git a/src/skytable.ts b/src/skytable.ts deleted file mode 100644 index 78a69c1..0000000 --- a/src/skytable.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Socket } from 'node:net'; -import { TLSSocket } from 'node:tls'; -import { connectionWrite } from './connection'; -import { encodeQuery, decodeResponse } from './protocol'; -import { Query } from './query'; - -export type ColumnBase = string | number | boolean | null | bigint; -export type SQParam = T | SQParam[]; -export type ColumnBinary = typeof Buffer; -export type ColumnList = T | ColumnList[]; -export type Column = - | Buffer - | ColumnBase - | ColumnBinary - | ColumnList; -export type Row = Column[]; -export type Rows = Row[]; -export type QueryResult = Column | Row | Rows; - -/** - * create a db instance - * @param connection The connection to Skytable - * @returns { query: (query: string | Query, ...params: SQParam[]) => Promise } - */ -export function createDB(connection: Socket | TLSSocket) { - const query = async ( - query: string | Query, - ...params: SQParam[] - ): Promise => { - const queryInstance = typeof query === 'string' ? new Query(query) : query; - params.forEach((param) => { - queryInstance.pushParam(param); - }); - const buffer = encodeQuery(queryInstance); - const res = await connectionWrite(connection, buffer); - return decodeResponse(res); - }; - return { - query, - }; -} diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..db62f7f --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,3 @@ +export function isFloat(number: number): boolean { + return Number.isFinite(number) && !Number.isInteger(number); +} From 3c814f008d31eb1e4578395a30f8a1dd222bbcbc Mon Sep 17 00:00:00 2001 From: Sayan Nandan Date: Thu, 28 Dec 2023 19:46:58 +0530 Subject: [PATCH 2/4] Add config module --- src/config.ts | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ src/protocol.ts | 2 +- 2 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 src/config.ts diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..8269076 --- /dev/null +++ b/src/config.ts @@ -0,0 +1,48 @@ +export class Config { + private username: string; + private password: string; + private host: string; + private port: number; + constructor( + username: string, + password: string, + host: string = '127.0.0.1', + port: number = 2003, + ) { + this.username = username; + this.password = password; + this.host = host; + this.port = port; + } + /** + * Get the set username + * @returns Username set in this configuration + */ + getUsername(): string { + return this.username; + } + + /** + * Get the set password + * @returns password set in this configuration + */ + getPassword(): string { + return this.password; + } + + /** + * Get the set host + * @returns host set in this configuration + */ + getHost(): string { + return this.host; + } + + /** + * Get the set port + * @returns port set in this configuration + */ + getPort(): number { + return this.port; + } +} diff --git a/src/protocol.ts b/src/protocol.ts index 01931aa..a5c7d73 100644 --- a/src/protocol.ts +++ b/src/protocol.ts @@ -11,7 +11,7 @@ export const PARAMS_TYPE = { STRING: new Uint8Array([0x06]), }; -export const NEWLINE = new Uint8Array([0x0A]); +export const NEWLINE = new Uint8Array([0x0a]); const STATICALLY_ENCODED_BOOL_FALSE = new Uint8Array([0x00]); const STATICALLY_ENCODED_BOOL_TRUE = new Uint8Array([0x01]); From aac2451f24c10be4614fb4cb5a4537a7f088927c Mon Sep 17 00:00:00 2001 From: Sayan Nandan Date: Fri, 29 Dec 2023 11:55:59 +0530 Subject: [PATCH 3/4] Refactor entire client library We make several changes including: - A separate `Connection` object - A special `Empty` class for empty response (to differentiate from null values) - Only allow query objects to be sent - Use a buffer list for all parameters --- src/config.ts | 26 +++++ src/connection.ts | 104 ++++++++++++++++++++ src/index.ts | 2 + src/protocol.ts | 245 +++++++++++++++++++++++++++++++++++++++++++--- src/query.ts | 12 ++- src/utils.ts | 23 +++++ 6 files changed, 394 insertions(+), 18 deletions(-) create mode 100644 src/connection.ts diff --git a/src/config.ts b/src/config.ts index 8269076..b3b7796 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,3 +1,6 @@ +import { ConnectionOptions } from 'tls'; +import { Connection, createTcp, createTls } from './connection'; + export class Config { private username: string; private password: string; @@ -45,4 +48,27 @@ export class Config { getPort(): number { return this.port; } + + /** + * Get a TCP connection to the database + * + * @returns a Skyhash/TCP connection + */ + async connect(): Promise { + const con = await createTcp(this); + await con._handshake(this); + return con; + } + + /** + * Get a TLS connection to the database + * + * @param tlsOpts TLS options + * @returns a Skyhash/TLS connection + */ + async connectTls(tlsOpts: ConnectionOptions): Promise { + const con = await createTls(this, tlsOpts); + await con._handshake(this); + return con; + } } diff --git a/src/connection.ts b/src/connection.ts new file mode 100644 index 0000000..06c6004 --- /dev/null +++ b/src/connection.ts @@ -0,0 +1,104 @@ +/** + * WARNING + * --- + * This is an incomplete implementation for the buffer read from the socket. + * + * It needs to be fixed so that if sufficient data is not buffered, we should enter a retry + * read loop (as is obvious). + * + * TODO(@ohsayan) + */ + +import { Socket, connect as connectTcp } from 'net'; +import { + TLSSocket, + connect as connectTcpTLS, + ConnectionOptions as TlsOptions, +} from 'tls'; +import { Query } from './query'; +import { + NEWLINE, + responseDecode, + Response, + handshakeEncode, + handshakeDecode, +} from './protocol'; +import { Config } from './config'; +import { connectionWrite } from './utils'; + +export function createTcp(c: Config): Promise { + return new Promise((resolve, reject) => { + const conn = connectTcp({ + port: c.getPort(), + host: c.getHost(), + }); + conn.once('connect', () => { + resolve(new Connection(conn)); + }); + conn.once('error', (error) => { + console.error(`tcp connection failed with error: ${error.message}`); + reject(error); + }); + }); +} +export function createTls(c: Config, tlsOpts: TlsOptions): Promise { + return new Promise((resolve, reject) => { + const conn = connectTcpTLS(tlsOpts); + conn.once('connect', () => { + resolve(new Connection(conn)); + }); + conn.once('error', (error) => { + console.error(`tls connection failed with error: ${error.message}`); + reject(error); + }); + }); +} + +export class Connection { + private socket: TLSSocket | Socket; + constructor(c: TLSSocket | Socket) { + this.socket = c; + } + async _handshake(c: Config): Promise { + const data = await connectionWrite(this.socket, handshakeEncode(c)); + await handshakeDecode(data); + } + public query(q: Query): Promise { + return new Promise((resolve, reject) => { + // Set listeners + const dataListener = (buf: Buffer) => { + this.socket.removeListener('error', errorListener); + resolve(responseDecode(buf)); + }; + const errorListener = (e: Error) => { + this.socket.removeListener('data', dataListener); + reject(e); + }; + this.socket.once('data', dataListener); + this.socket.once('error', errorListener); + // Calculate dataframe size + const queryBuffer = q.getQuery(); + const paramsBuffer = q._getParamBuffer(); + const qWindow = queryBuffer.length.toString(); + // Calculate packet size + const packetSize = + qWindow.length + + 1 + + queryBuffer.length + + paramsBuffer.reduce((acc, buf) => acc + buf.length, 0); + // send data + this.socket.write("S"); + this.socket.write(packetSize.toString()); + this.socket.write(NEWLINE); + this.socket.write(qWindow); + this.socket.write(NEWLINE); + this.socket.write(queryBuffer); + paramsBuffer.forEach((buf) => this.socket.write(buf)); + }); + } + public async disconnect() { + if (this.socket) { + this.socket.destroySoon(); + } + } +} diff --git a/src/index.ts b/src/index.ts index 2a15f56..665e870 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1 +1,3 @@ export { Query, Parameter, SignedInt } from './query'; +export { Connection } from './connection'; +export { Config } from './config'; diff --git a/src/protocol.ts b/src/protocol.ts index a5c7d73..29cb49a 100644 --- a/src/protocol.ts +++ b/src/protocol.ts @@ -1,6 +1,49 @@ +import { Config } from './config'; import { Parameter, Query, isSQParam } from './query'; import { isFloat } from './utils'; +/* + Handshake +*/ + +const HANDSHAKE_RESULT = { + SUCCESS: 'H00', + ERROR: 'H01', +}; + +export function handshakeEncode(config: Config): string { + const username = config.getUsername(); + const password = config.getPassword(); + return [ + 'H\x00\x00\x00\x00\x00', + username.length, + '\n', + password.length, + '\n', + username, + password, + ].join(''); +} + +export function handshakeDecode(buffer: Buffer): Promise { + return new Promise((resolve, reject) => { + const [h, c1, c2, msg] = Array.from(buffer.toJSON().data); + const code = [String.fromCharCode(h), c1, c2].join(''); + if (code === HANDSHAKE_RESULT.SUCCESS) { + return resolve(); + } + reject(new Error(`handshake error code ${code}, msg: ${msg}`)); + }); +} + +/* + Query implementations +*/ + +export const NEWLINE = new Uint8Array([0x0a]); +const STATICALLY_ENCODED_BOOL_FALSE = new Uint8Array([0x00]); +const STATICALLY_ENCODED_BOOL_TRUE = new Uint8Array([0x01]); + export const PARAMS_TYPE = { NULL: new Uint8Array([0x00]), BOOLEAN: new Uint8Array([0x01]), @@ -11,15 +54,11 @@ export const PARAMS_TYPE = { STRING: new Uint8Array([0x06]), }; -export const NEWLINE = new Uint8Array([0x0a]); -const STATICALLY_ENCODED_BOOL_FALSE = new Uint8Array([0x00]); -const STATICALLY_ENCODED_BOOL_TRUE = new Uint8Array([0x01]); - -export function encodeParams(query: Query, param: Parameter): void { +export function queryEncodeParams(query: Query, param: Parameter): void { switch (typeof param) { case 'boolean': { query - .getParamBuffer() + ._getParamBuffer() .push( Buffer.concat([ PARAMS_TYPE.BOOLEAN, @@ -32,7 +71,7 @@ export function encodeParams(query: Query, param: Parameter): void { } case 'number': { query - .getParamBuffer() + ._getParamBuffer() .push( Buffer.concat([ isFloat(param) @@ -48,7 +87,7 @@ export function encodeParams(query: Query, param: Parameter): void { } case 'bigint': { query - .getParamBuffer() + ._getParamBuffer() .push( Buffer.concat([ param < 0 ? PARAMS_TYPE.SINT : PARAMS_TYPE.UINT, @@ -60,17 +99,17 @@ export function encodeParams(query: Query, param: Parameter): void { } case 'string': { query - .getParamBuffer() + ._getParamBuffer() .push(Buffer.concat([PARAMS_TYPE.STRING, Buffer.from(param), NEWLINE])); break; } case 'object': { if (param === null) { - query.getParamBuffer().push(PARAMS_TYPE.NULL); + query._getParamBuffer().push(PARAMS_TYPE.NULL); break; } else if (param instanceof Buffer) { query - .getParamBuffer() + ._getParamBuffer() .push( Buffer.concat([ PARAMS_TYPE.BINARY, @@ -81,13 +120,191 @@ export function encodeParams(query: Query, param: Parameter): void { ); break; } else if (isSQParam(param)) { - return query.incrQueryCountBy( - param.encodeUnsafe(query.getParamBuffer()), + return query._incrQueryCountBy( + param.encodeUnsafe(query._getParamBuffer()), ); } } default: throw new TypeError(`unsupported type: ${typeof param}, val: ${param}`); } - query.incrQueryCountBy(1); + query._incrQueryCountBy(1); +} + +/* + response implementations +*/ + +export type SimpleValue = null | boolean | number | bigint | Buffer | string; +export type Value = SimpleValue | Value[]; +export type Row = Value[]; +export type Rows = Row[]; +export type Response = Value | Row | Rows | Empty; + +/** + * An empty response, usually indicative of a succesful action (much like HTTP 200) + */ +export class Empty { + constructor() {} +} + +const RESPONSES_RESULT = { + NULL: 0, + BOOL: 1, + U8INT: 2, + U16INT: 3, + U32INT: 4, + U64INT: 5, + S8INT: 6, + S16INT: 7, + S32INT: 8, + S64INT: 9, + FLOAT32: 10, + FLOAT64: 11, + BINARY: 12, + STRING: 13, + LIST: 14, + ERROR: 0x10, + ROW: 0x11, + EMPTY: 0x12, + MULTIROW: 0x13, +}; + +function getFirstSplitOffset(buffer: Buffer, split = '\n'): number { + for (let i = 0; i < buffer.length; i++) { + if (buffer[i] === split.charCodeAt(0)) { + return i; + } + } + return -1; +} + +function parseNumber( + formatFn: (string: string) => T, + buffer: Buffer, +): [T, Buffer] { + const offset = getFirstSplitOffset(buffer); + const val = formatFn(buffer.subarray(0, offset).toString('utf-8')); + return [val, buffer.subarray(offset + 1)]; +} + +function parseNextBySize(size: number, buffer: Buffer): [Value[], Buffer] { + let values = []; + let nextBuffer = buffer; + for (let i = 0; i < size; i++) { + const [value, remainingBuffer] = decodeValue(nextBuffer); + values.push(value); + nextBuffer = remainingBuffer; + } + return [values, nextBuffer]; +} + +function decodeValue(buffer: Buffer): [Value, Buffer] { + const type = buffer.readUInt8(0); + buffer = buffer.subarray(1); + switch (type) { + case RESPONSES_RESULT.NULL: // Null + return [null, buffer.subarray(0)]; + case RESPONSES_RESULT.BOOL: // Bool + return [Boolean(buffer.readUInt8(0)), buffer.subarray(1)]; + case RESPONSES_RESULT.U8INT: // 8-bit Unsigned Integer + return parseNumber(Number, buffer); + case RESPONSES_RESULT.U16INT: // 16-bit Unsigned Integer + return parseNumber(Number, buffer); + case RESPONSES_RESULT.U32INT: // 32-bit Unsigned Integer + return parseNumber(Number, buffer); + case RESPONSES_RESULT.U64INT: // 64-bit Unsigned Integer + return parseNumber(BigInt, buffer); + case RESPONSES_RESULT.S8INT: // 8-bit Signed Integer + return parseNumber(Number, buffer); + case RESPONSES_RESULT.S16INT: // 16-bit Signed Integer + return parseNumber(Number, buffer); + case RESPONSES_RESULT.S32INT: // 32-bit Signed Integer + return parseNumber(Number, buffer); + case RESPONSES_RESULT.S64INT: // 64-bit Signed Integer + return parseNumber(BigInt, buffer); + case RESPONSES_RESULT.FLOAT32: // f32 + return parseNumber(Number.parseFloat, buffer); + case RESPONSES_RESULT.FLOAT64: // f64 + return parseNumber(Number.parseFloat, buffer); + case RESPONSES_RESULT.BINARY: { + // Binary \n, + const sizeOffset = getFirstSplitOffset(buffer); + const size = Number(buffer.subarray(0, sizeOffset).toString('utf-8')); + if (size === 0) { + return [Buffer.from([]), buffer.subarray(sizeOffset + 1)]; + } + const [start, end] = [sizeOffset + 1, sizeOffset + 1 + Number(size)]; + return [buffer.subarray(start, end), buffer.subarray(end)]; + } + case RESPONSES_RESULT.STRING: { + // String \n + const sizeOffset = getFirstSplitOffset(buffer); + const size = Number(buffer.subarray(0, sizeOffset).toString('utf-8')); + const [start, end] = [sizeOffset + 1, sizeOffset + 1 + Number(size)]; + const str = buffer.subarray(start, end).toString('utf-8'); + return [str, buffer.subarray(end)]; + } + case RESPONSES_RESULT.LIST: { + // List \n + const sizeOffset = getFirstSplitOffset(buffer); + const size = Number(buffer.subarray(0, sizeOffset).toString('utf-8')); + if (size === 0) { + return [[], buffer.subarray(sizeOffset + 1)]; + } + return parseNextBySize(size, buffer.subarray(sizeOffset + 1)) as [ + Value, + Buffer, + ]; + } + default: + throw new Error(`Unknown data type: ${type}`); + } +} + +function decodeRow(buffer: Buffer): Row { + const offset = getFirstSplitOffset(buffer); + const columnCount = Number(buffer.subarray(0, offset).toString('utf-8')); + const dataType = buffer.subarray(offset + 1); + const [row] = parseNextBySize(columnCount, dataType); + return row; +} + +function decodeRows(buffer: Buffer): Rows { + const offset = getFirstSplitOffset(buffer); + const rowCount = Number(buffer.subarray(0, offset).toString('utf-8')); + buffer = buffer.subarray(offset + 1); + const columnOffset = getFirstSplitOffset(buffer); + const columnCount = Number( + buffer.subarray(0, columnOffset).toString('utf-8'), + ); + buffer = buffer.subarray(columnOffset + 1); + const result: Rows = []; + let nextBuffer = buffer; + for (let i = 0; i < rowCount; i++) { + const [row, remainingBuffer] = parseNextBySize(columnCount, nextBuffer); + result[i] = row; + nextBuffer = remainingBuffer; + } + return result; +} + +export function responseDecode(buffer: Buffer): Response { + const type = buffer.readInt8(0); + switch (type) { + case RESPONSES_RESULT.EMPTY: + return new Empty(); + case RESPONSES_RESULT.ROW: + return decodeRow(buffer.subarray(1)); + case RESPONSES_RESULT.MULTIROW: + return decodeRows(buffer.subarray(1)); + case RESPONSES_RESULT.ERROR: + throw new Error( + `response error code: ${buffer.subarray(1, 2).readInt8()}`, + ); + default: + break; + } + const [val] = decodeValue(buffer); + return val; } diff --git a/src/query.ts b/src/query.ts index 6c64c68..2128e1e 100644 --- a/src/query.ts +++ b/src/query.ts @@ -1,4 +1,8 @@ -import { encodeParams as encodeParam, NEWLINE, PARAMS_TYPE } from './protocol'; +import { + queryEncodeParams as encodeParam, + NEWLINE, + PARAMS_TYPE, +} from './protocol'; import { isFloat } from './utils'; /** @@ -42,7 +46,7 @@ export class Query { * Get the base query payload * @returns base query */ - public getQuery(): String { + public getQuery(): string { return this.query; } /** @@ -64,7 +68,7 @@ export class Query { * * @returns returns the parameter buffer */ - getParamBuffer(): Uint8Array[] { + _getParamBuffer(): Uint8Array[] { return this.params; } /** @@ -72,7 +76,7 @@ export class Query { * * @param by newly added param count */ - incrQueryCountBy(by: number): void { + _incrQueryCountBy(by: number): void { this.param_cnt += by; } } diff --git a/src/utils.ts b/src/utils.ts index db62f7f..3bff77d 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,3 +1,26 @@ +import { Socket } from 'net'; +import { TLSSocket } from 'tls'; + export function isFloat(number: number): boolean { return Number.isFinite(number) && !Number.isInteger(number); } + +export function connectionWrite( + connect: Socket | TLSSocket, + buffer: Buffer | string, +): Promise { + return new Promise((resolve, reject) => { + connect.write(buffer, (writeError) => { + if (writeError) { + reject(writeError); + return; + } + connect.once('data', (data) => { + resolve(data); + }); + connect.once('error', (err) => { + reject(err); + }); + }); + }); +} From dd1c0e6c809cf669984586e67393b5660d1a05bc Mon Sep 17 00:00:00 2001 From: Sayan Nandan Date: Fri, 29 Dec 2023 12:19:54 +0530 Subject: [PATCH 4/4] Fix query packet generation and update README --- README.md | 38 +++++++++---- __tests__/dcl.spec.ts | 20 +++++++ __tests__/ddl.spec.ts | 73 ------------------------- __tests__/dml.spec.ts | 122 ------------------------------------------ __tests__/utils.ts | 25 --------- examples/simple.js | 19 +++++++ examples/simple.ts | 36 ------------- package.json | 2 +- src/connection.ts | 2 +- src/index.ts | 1 + 10 files changed, 70 insertions(+), 268 deletions(-) create mode 100644 __tests__/dcl.spec.ts delete mode 100644 __tests__/ddl.spec.ts delete mode 100644 __tests__/dml.spec.ts delete mode 100644 __tests__/utils.ts create mode 100644 examples/simple.js delete mode 100644 examples/simple.ts diff --git a/README.md b/README.md index cf7068d..3cc06d0 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,34 @@ -# Skytable NodeJS Driver +# `skytable-node`: Skytable driver for NodeJS -This is the skeleton template repository for Skytable's NodeJS driver. You can [track the implementation here](https://github.com/skytable/skytable/issues/324)! +## Getting started -Tasks: -- [ ] Implement a basic Skytable client driver for NodeJS -- [ ] Functionality: - - [ ] Implement the full Skyhash protocol - - [ ] Be able to send and receive queries and decode them ergonomically (into appropriate objects and/or types) - - [ ] Should have a way to use TLS +```shell +yarn add skytable-node +``` -> For contributors: You might find the [Rust client driver](https://github.com/skytable/client-rust) to be a good reference. -> We're here to help! Please jump into our [Discord Community](https://discord.gg/QptWFdx); we're here to mentor you. +## Example + +```js +const { Config, Query } = require('skytable-node'); +const cfg = new Config("root", "password"); + +async function main() { + let db; + try { + db = await cfg.connect(); + console.log(await db.query(new Query("sysctl report status"))); + } catch (e) { + console.error(e); + process.exit(1); + } finally { + if (db) { + await db.disconnect(); + } + } +} + +main() +``` ## License diff --git a/__tests__/dcl.spec.ts b/__tests__/dcl.spec.ts new file mode 100644 index 0000000..29cab59 --- /dev/null +++ b/__tests__/dcl.spec.ts @@ -0,0 +1,20 @@ +import { Config, Query } from '../src'; + +const cfg = new Config('root', 'password'); + +async function main() { + let db; + try { + db = await cfg.connect(); + console.log(await db.query(new Query('sysctl report status'))); + } catch (e) { + console.error(e); + process.exit(1); + } finally { + if (db) { + await db.disconnect(); + } + } +} + +main(); diff --git a/__tests__/ddl.spec.ts b/__tests__/ddl.spec.ts deleted file mode 100644 index 487c245..0000000 --- a/__tests__/ddl.spec.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { getDBConfig, getSpace, getTable } from './utils'; - -const testSpace = 'ddltestspace'; - -describe('DDL', () => { - let db: any; - const dbConfig = getDBConfig(); - - beforeAll(async () => { - db = await dbConfig.connect(); - }); - - afterAll(async () => { - dbConfig.disconnect(); - }); - - it('CREATE SPACE', async () => { - const spaceName = `${testSpace + Date.now()}`; - try { - expect(await db.query(`CREATE SPACE ${spaceName}`)).toBe(null); - } finally { - await db.query(`DROP SPACE ALLOW NOT EMPTY ${spaceName}`); - } - }); - - // FIXME need to fix. why need 'ALTER SPACE'? - // it('ALTER SPACE', async () => { - // const spaceName = `${testSpace + Date.now()}`; - // try { - // await db.query(`CREATE SPACE IF NOT EXISTS ${spaceName} WITH { property_name: ? }`, 123); - - // expect(await db.query(`ALTER SPACE ${spaceName} WITH { property_name: ? }`, 456)).toBe(null); - // } finally { - // await db.query(`DROP SPACE ALLOW NOT EMPTY ${spaceName}`); - // } - // }) - - it('CREATE MODEL', async () => { - const [space, drop] = await getSpace(db, `${testSpace + Date.now()}`); - const tableName = `${space}.testTable${Date.now()}`; - - try { - await db.query(`CREATE MODEL ${tableName}(id: string, name: string)`); - - expect( - await db.query( - `CREATE MODEL IF NOT EXISTS ${tableName}(id: string, name: string)`, - ), - ).toBe(false); - } finally { - await drop(); - } - }); - - it('ALTER MODEL', async () => { - const [tableName, drop] = await getTable(db); - - try { - await db.query(`CREATE MODEL ${tableName}(id: string, name: string)`); - await db.query(`ALTER MODEL ${tableName} ADD field { type: uint8 }`); - await db.query( - `ALTER MODEL ${tableName} ADD ( first_field { type: string }, second_field { type: binary } )`, - ); - - await db.query(`ALTER MODEL ${tableName} UPDATE field { type: uint64 }`); - await db.query( - `ALTER MODEL ${tableName} REMOVE (first_field, second_field)`, - ); - } finally { - await drop(); - } - }); -}); diff --git a/__tests__/dml.spec.ts b/__tests__/dml.spec.ts deleted file mode 100644 index 4640e4b..0000000 --- a/__tests__/dml.spec.ts +++ /dev/null @@ -1,122 +0,0 @@ -import type { Rows } from '../src'; -import { getDBConfig, getTable } from './utils'; - -describe('DML', () => { - let db: any; - const dbConfig = getDBConfig(); - - beforeAll(async () => { - db = await dbConfig.connect(); - }); - - afterAll(async () => { - dbConfig.disconnect(); - }); - - it('null type', async () => { - const [tableName, drop] = await getTable(db); - try { - await db.query( - `CREATE MODEL ${tableName}(username: string, null email_id: string)`, - ); - - await db.query(`INSERT INTO ${tableName}(?, ?)`, 'test', null); - - expect( - await db.query( - `SELECT username,email_id FROM ${tableName} WHERE username = ?`, - 'test', - ), - ).toEqual(['test', null]); - } finally { - await drop(); - } - }); - - it('int number type', async () => { - const [tableName, drop] = await getTable(db); - - try { - await db.query( - `CREATE MODEL ${tableName}(u8: uint8, u16: uint16, u32: uint32, u64: uint64)`, - ); - - await db.query( - `INSERT INTO ${tableName}(?, ?, ?, ?)`, - 1, - 2, - 3312321, - BigInt(478787872837218382), - ); - - // TODO why is the uint8 in bigint - expect( - await db.query(`SELECT * FROM ${tableName} WHERE u8 = ?`, 1), - ).toEqual([BigInt(1), 2, 3312321, BigInt(478787872837218382)]); - } finally { - await drop(); - } - }); - - it('list type', async () => { - const [tableName, drop] = await getTable(db); - - try { - await db.query( - `CREATE MODEL ${tableName}(id: uint64, list: list { type: string} )`, - ); - - await db.query(`INSERT INTO ${tableName}(?, [?])`, 1, 'test'); - - expect( - await db.query(`SELECT * FROM ${tableName} WHERE id = ?`, 1), - ).toEqual([BigInt(1), ['test']]); - } finally { - await drop(); - } - }); - - it('binary type', async () => { - const [tableName, drop] = await getTable(db); - - try { - await db.query(`CREATE MODEL ${tableName}(id: uint64, binary: binary )`); - - await db.query(`INSERT INTO ${tableName}(?, ?)`, 1, Buffer.from('test')); - - expect( - await db.query(`SELECT * FROM ${tableName} WHERE id = ?`, 1), - ).toEqual([BigInt(1), Buffer.from('test')]); - } finally { - await drop(); - } - }); - - it('Multirow', async () => { - const [tableName, drop] = await getTable(db); - - try { - await db.query(`CREATE MODEL ${tableName}(id: string, name: string )`); - for (let i = 0; i < 10; i++) { - await db.query(`INSERT INTO ${tableName}(?, ?)`, String(i), 'test'); - } - const rows = (await db.query( - `SELECT ALL * FROM ${tableName} LIMIT ?`, - 10, - )) as Rows; - - expect( - rows.sort((row1, row2) => - Number(row1[0] as string) > Number(row2[0] as string) ? 1 : -1, - ), - ).toEqual([ - ...Array.from({ length: 10 }).map((_, index) => [ - String(index), - 'test', - ]), - ]); - } finally { - await drop(); - } - }); -}); diff --git a/__tests__/utils.ts b/__tests__/utils.ts deleted file mode 100644 index 9811fbb..0000000 --- a/__tests__/utils.ts +++ /dev/null @@ -1,25 +0,0 @@ -import Config from '../src'; - -export function getDBConfig() { - return new Config('root', 'admin123456123456', '127.0.0.1', 2003); -} - -export async function getSpace( - db: any, - space = 'testspace', -): Promise<[string, Function]> { - await db.query(`CREATE SPACE IF NOT EXISTS ${space}`); - - await db.query(`USE ${space}`); - - return [ - space, - async () => await db.query(`DROP SPACE ALLOW NOT EMPTY ${space}`), - ]; -} - -export async function getTable(db: any): Promise<[string, Function]> { - const [space, drop] = await getSpace(db, `testTable${Date.now()}Space`); - - return [`${space}.testTable${Date.now()}`, drop as Function]; -} diff --git a/examples/simple.js b/examples/simple.js new file mode 100644 index 0000000..b6f1104 --- /dev/null +++ b/examples/simple.js @@ -0,0 +1,19 @@ +const { Config, Query } = require('skytable-node'); +const cfg = new Config('root', 'password'); + +async function main() { + let db; + try { + db = await cfg.connect(); + console.log(await db.query(new Query('sysctl report status'))); + } catch (e) { + console.error(e); + process.exit(1); + } finally { + if (db) { + await db.disconnect(); + } + } +} + +main(); diff --git a/examples/simple.ts b/examples/simple.ts deleted file mode 100644 index 6a0c2af..0000000 --- a/examples/simple.ts +++ /dev/null @@ -1,36 +0,0 @@ -import Config, { type Row } from '../src'; - -async function main() { - const config = new Config('root', 'admin123456123456', '127.0.0.1', 2003); - const spaceName = `testSpace`; - const tableName = `${spaceName}.users`; - const db = await config.connect(); - - try { - await db.query(`create space IF NOT EXISTS ${spaceName}`); - await db.query(`use ${spaceName}`); - await db.query( - `CREATE MODEL ${tableName}(username: string, password: string, null email_id: string)`, - ); - await db.query( - `INSERT INTO ${tableName}(?, ?, ?)`, - 'test', - 'password', - null, - ); - const row = await db.query( - `SELECT * FROM ${tableName} WHERE username = ?`, - 'test', - ); - const [username, password, email_id] = row as Row; - console.assert(username === 'test'); - console.assert(password === 'password'); - console.assert(email_id == null); - } catch (e) { - console.error(e); - } finally { - config.disconnect(); - } -} - -main(); diff --git a/package.json b/package.json index 5771fc8..3be511a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "skytable-node", - "version": "0.1.2", + "version": "0.2.0", "main": "dist/cjs/index.js", "types": "dist/cjs/index.d.ts", "module": "dist/esm/index.mjs", diff --git a/src/connection.ts b/src/connection.ts index 06c6004..8bddefe 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -87,7 +87,7 @@ export class Connection { queryBuffer.length + paramsBuffer.reduce((acc, buf) => acc + buf.length, 0); // send data - this.socket.write("S"); + this.socket.write('S'); this.socket.write(packetSize.toString()); this.socket.write(NEWLINE); this.socket.write(qWindow); diff --git a/src/index.ts b/src/index.ts index 665e870..7beff7c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,4 @@ export { Query, Parameter, SignedInt } from './query'; export { Connection } from './connection'; export { Config } from './config'; +export { SimpleValue, Value, Row, Rows, Empty } from './protocol';