mirror of
https://github.com/zhigang1992/DefinitelyTyped.git
synced 2026-04-13 08:57:26 +08:00
Add node-libpq typings (#14044)
* add type definition for package: libpq * style(libpq): change indent size to 4 white-spaces * test(libpq): add tests for libpq * test(libpq): more tests for libpq * chore(libpq): create project by 'npm run new-project' script * test(libpq): fix call signature * fix(libpq): update in accordance with test information * doc(libpq): improve documentation in accordance with typedoc * fix(libpq): connection callback argument type should be Error, not string * doc(libpq): stylize documentation * feat(libpq): add exporting inner interfaces
This commit is contained in:
committed by
Mohamed Hegazy
parent
a661f615b6
commit
00221afddf
428
libpq/index.d.ts
vendored
Normal file
428
libpq/index.d.ts
vendored
Normal file
@@ -0,0 +1,428 @@
|
||||
// Type definitions for libpq 1.8
|
||||
// Project: https://github.com/brianc/node-libpq#readme
|
||||
// Definitions by: Vlad Rindevich <https://github.com/Lodin>
|
||||
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
|
||||
|
||||
/// <reference types="node" />
|
||||
|
||||
import {EventEmitter} from 'events';
|
||||
import {Buffer} from 'buffer';
|
||||
|
||||
declare namespace Libpq {
|
||||
export interface NotifyMsg {
|
||||
relname: string;
|
||||
extra: string;
|
||||
be_pid: number;
|
||||
}
|
||||
|
||||
export interface ResultError {
|
||||
severity: string;
|
||||
sqlState: string;
|
||||
messagePrimary: string;
|
||||
messageDetail?: string;
|
||||
messageHint?: string;
|
||||
statementPosition?: string;
|
||||
internalPosition?: string;
|
||||
internalQuery?: string;
|
||||
context?: string;
|
||||
schemaName?: string;
|
||||
tableName?: string;
|
||||
dataTypeName?: string;
|
||||
constraintName?: string;
|
||||
sourceFile: string;
|
||||
sourceLine: string;
|
||||
sourceFunction: string;
|
||||
}
|
||||
}
|
||||
|
||||
declare class Libpq extends EventEmitter {
|
||||
|
||||
/**
|
||||
* Current connection state.
|
||||
*/
|
||||
connected: boolean;
|
||||
|
||||
/**
|
||||
* Issues a request to cancel the currently executing query on this instance of libpq.
|
||||
*
|
||||
* @returns {(boolean|string)} true if the cancel request was sent; a string error message if
|
||||
* the cancel request failed for any reason. The string will
|
||||
* contain the error message provided by libpq.
|
||||
*/
|
||||
cancel(): true|string;
|
||||
|
||||
/**
|
||||
* Manually frees the memory associated with a PGresult pointer. Generally this is called
|
||||
* for you, but if you absolutely want to free the pointer yourself, you can.
|
||||
*/
|
||||
clear(): void;
|
||||
|
||||
/**
|
||||
* @returns {string} the status string associated with a result. Something akin to INSERT 3 0
|
||||
* if you inserted 3 rows.
|
||||
*/
|
||||
cmdStatus(): string;
|
||||
|
||||
/**
|
||||
* @returns {string} the number of tuples (rows) affected by the command. Even though this is a
|
||||
* number, it is returned as a string to mirror libpq's behavior.
|
||||
*/
|
||||
cmdTuples(): string;
|
||||
|
||||
/**
|
||||
* (async) Connects to a PostgreSQL backend server process.
|
||||
*
|
||||
* This function actually calls the PQconnectdb blocking connection method in a background
|
||||
* thread within node's internal thread-pool. There is a way to do non-blocking network I/O
|
||||
* for some of the connecting with libpq directly, but it still blocks when your local file
|
||||
* system looking for config files, SSL certificates, .pgpass file, and doing possible dns
|
||||
* resolution. Because of this, the best way to get fully non-blocking is to juse use
|
||||
* libuv_queue_work and let node do it's magic and so that's what I do. This function does
|
||||
* not block.
|
||||
*
|
||||
* @param {string} connectParams an optional string
|
||||
* @param {Function} callback mandatory. It is called when the connection has successfully been
|
||||
* established.
|
||||
*/
|
||||
connect(connectParams: string, callback: (err?: Error) => void): void;
|
||||
connect(callback: (err?: Error) => void): void;
|
||||
|
||||
/**
|
||||
* (sync) Attempts to connect to a PostgreSQL server. BLOCKS until it either succeedes, or
|
||||
* fails. If it fails it will throw an exception.
|
||||
*
|
||||
* @param {string} connectionParams an optional string
|
||||
*/
|
||||
connectSync(connectionParams?: string): void;
|
||||
|
||||
/**
|
||||
* Reads waiting data from the socket. If the socket is not readable and you call this it will
|
||||
* block so be careful and only call it within the readable callback for the most part.
|
||||
*
|
||||
* @returns {boolean} true if data was read; false if there was an error. You can access
|
||||
* error details with [[Libpq.errorMessage]].
|
||||
*/
|
||||
consumeInput(): boolean
|
||||
|
||||
/**
|
||||
* Retrieves the last error message from the connection. This is intended to be used after most
|
||||
* functions which return an error code to get more detailed error information about the
|
||||
* connection. You can also check this before issuing queries to see if your connection has
|
||||
* been lost.
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
errorMessage(): string;
|
||||
|
||||
/**
|
||||
* Exact copy of the PQescapeIdentifier function within libpq. Requires an established
|
||||
* connection but does not perform any I/O.
|
||||
*
|
||||
* @param {string} input
|
||||
*/
|
||||
escapeIdentifier(input: string): string;
|
||||
|
||||
/**
|
||||
* Exact copy of the PQescapeLiteral function within libpq. Requires an established connection
|
||||
* but does not perform any I/O.
|
||||
*
|
||||
* @param {string} input
|
||||
*/
|
||||
escapeLiteral(input: string): string;
|
||||
|
||||
/**
|
||||
* (sync) Sends a command to the backend and blocks until a result is received.
|
||||
*
|
||||
* @param {string} [commandText=""] a required string of the query.
|
||||
*/
|
||||
exec(commandText?: string): void;
|
||||
|
||||
/**
|
||||
* (sync) Sends a command and parameters to the backend and blocks until a result is received.
|
||||
*
|
||||
* @param {string} [commandText=""] a required string of the query.
|
||||
* @param {Array.<(string|number)>} [parameters=[]] a required array of string values
|
||||
* corresponding to each parameter in the
|
||||
* commandText.
|
||||
*/
|
||||
execParams(commandText?: string, parameters?: Array<string|number>): void;
|
||||
|
||||
/**
|
||||
* (sync) Sends a command to the server to execute a previously prepared statement. Blocks
|
||||
* until the results are returned.
|
||||
*
|
||||
* @param {string} [statementName=""] a required string of the name of the prepared statement.
|
||||
* @param {Array.<(string|number)>} [parameters=[]] the parameters to pass to the prepared
|
||||
* statement.
|
||||
*/
|
||||
execPrepared(statementName?: string, parameters?: Array<string|number>): void;
|
||||
|
||||
/**
|
||||
* Disconnects from the backend and cleans up all memory used by the libpq connection.
|
||||
*/
|
||||
finish(): void;
|
||||
|
||||
/**
|
||||
* Flushes buffered data to the socket.
|
||||
*
|
||||
* @returns {number} 1 if socket is not write-ready at which case you should call
|
||||
* [[Libpq.writable]] with a callback and wait for the socket to be writable
|
||||
* and then call [[Libpq.flush]] again; 0 if all data was flushed; -1 if
|
||||
* there was an error.
|
||||
*/
|
||||
flush(): number;
|
||||
|
||||
/**
|
||||
* Retrieve the name of the field (column) at the given offset. Offset starts at 0.
|
||||
*
|
||||
* @param {number} fieldNumber
|
||||
* @returns {string}
|
||||
*/
|
||||
fname(fieldNumber: number): string;
|
||||
|
||||
/**
|
||||
* Retrieve the Oid of the field (column) at the given offset. Offset starts at 0.
|
||||
*
|
||||
* @param {number} fieldNumber
|
||||
* @returns {number}
|
||||
*/
|
||||
ftype(fieldNumber: number): number;
|
||||
|
||||
/**
|
||||
* After issuing a successfuly command like COPY table TO stdout gets copy data from the
|
||||
* connection.
|
||||
*
|
||||
* @param {boolean} [async=false] a boolean. Pass false to block waiting for data from the
|
||||
* backend. Defaults to false.
|
||||
*
|
||||
* @returns {Buffer|number} a node buffer if there is data available; 0 if the copy is still in
|
||||
* progress (only if you have called [[Libpq.setNonBlocking]](true));
|
||||
* -1 if the copy is completed; -2 if there was an error.
|
||||
*/
|
||||
getCopyData(async?: boolean): Buffer|number;
|
||||
|
||||
/**
|
||||
* @param {number} tupleNumber
|
||||
* @param {number} fieldNumber
|
||||
* @returns {boolean} true if the value at the given offsets is actually null. Otherwise
|
||||
* returns false. This is because [[Libpq.getvalue]] returns an empty
|
||||
* string for both an actual empty string and for a null value. Weird, huh?
|
||||
*/
|
||||
getisnull(tupleNumber: number, fieldNumber: number): boolean;
|
||||
|
||||
/**
|
||||
* Parses received data from the server into a PGresult struct and sets a pointer internally to
|
||||
* the connection object to this result.
|
||||
*
|
||||
* Warning: this function will block if libpq is waiting on async results to be returned from
|
||||
* the server. Call [[Libpq.isBusy]] to determine if this command will block.
|
||||
*
|
||||
* @returns {boolean} true if libpq was able to read buffered data & parse a result object;
|
||||
* false if there are no results waiting to be parsed. Generally doing async
|
||||
* style queries you'll call this repeadedly until it returns false and then
|
||||
* use the result accessor methods to pull results out of the current result
|
||||
* set.
|
||||
*/
|
||||
getResult(): boolean;
|
||||
|
||||
/**
|
||||
* Retrieve the text value at a given tuple (row) and field (column) offset. Both offsets start
|
||||
* at 0. A null value is returned as the empty string ''.
|
||||
*
|
||||
* @param {number} tupleNumber
|
||||
* @param {number} [fieldNumber]
|
||||
* @returns {string}
|
||||
*/
|
||||
getvalue(tupleNumber: number, fieldNumber?: number): string;
|
||||
|
||||
/**
|
||||
* @returns {boolean} true if calling [[Libpq.consumeInput]] would block waiting for more
|
||||
* data; false if all data has been read from the socket. Once this returns false it is
|
||||
* safe to call [[Libpq.getResult]].
|
||||
*/
|
||||
isBusy(): boolean;
|
||||
|
||||
/**
|
||||
* @returns {boolean} true if non-blocking mode is enabled; false if disabled.
|
||||
*/
|
||||
isNonBlocking(): boolean;
|
||||
|
||||
/**
|
||||
* Retrieve the number of fields (columns) from the result.
|
||||
*
|
||||
* @returns {number}
|
||||
*/
|
||||
nfields(): number;
|
||||
|
||||
/**
|
||||
* Checks for NOTIFY messages that have come in. If any have been received they will be in the
|
||||
* following format:
|
||||
*
|
||||
* @example ```ts
|
||||
*
|
||||
* var msg = {
|
||||
* relname: 'name of channel',
|
||||
* extra: 'message passed to notify command',
|
||||
* be_pid: 130
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @returns {Libpq.NotifyMsg}
|
||||
*/
|
||||
notifies(): Libpq.NotifyMsg;
|
||||
|
||||
/**
|
||||
* Retrieve the number of tuples (rows) from the result.
|
||||
*
|
||||
* @returns {number}
|
||||
*/
|
||||
ntuples(): number;
|
||||
|
||||
/**
|
||||
* After issuing a successful command like COPY table FROM stdin you can start putting buffers
|
||||
* directly into the databse with this function.
|
||||
*
|
||||
* @param {Buffer} buffer a required node buffer of text data such as
|
||||
* Buffer('column1\tcolumn2\n')
|
||||
*
|
||||
* @returns {number} 1 if sent succesfully; 0 if the command would block (only if you have
|
||||
* called [[Libpq.setNonBlocking]](true)); -1 if there was an error sending
|
||||
* the command.
|
||||
*/
|
||||
putCopyData(buffer: Buffer): number;
|
||||
|
||||
/**
|
||||
* Signals the backed your copy procedure is complete. If you pass errorMessage it will be sent
|
||||
* to the backend and effectively cancel the copy operation.
|
||||
*
|
||||
* @param {string} [errorMessage] an optional string you can pass to cancel the copy operation.
|
||||
*
|
||||
* @returns {number} 1 if sent succesfully; 0 if the command would block (only if you have
|
||||
* called [[Libpq.setNonBlocking]](true)); -1 if there was an error sending
|
||||
* the command.
|
||||
*/
|
||||
putCopyEnd(errorMessage?: string): number;
|
||||
|
||||
/**
|
||||
* (sync) Sends a named statement to the server to be prepared for later execution. blocks
|
||||
* until a result from the prepare operation is received.
|
||||
*
|
||||
* @param {string} [statementName=""] a required string of name of the statement to prepare.
|
||||
* @param {string} [commandText=""] a required string of the query.
|
||||
* @param {number} [nParams=0] a count of the number of parameters in the commandText.
|
||||
*/
|
||||
prepare(statementName?: string, commandText?: string, nParams?: number): void;
|
||||
|
||||
/**
|
||||
* Retrieves detailed error information from the current result object. Very similar to
|
||||
* PQresultErrorField() except instead of passing a fieldCode and retrieving a single field,
|
||||
* retrieves all fields from the error at once on a single object. The object returned is a
|
||||
* simple hash, not an instance of an error object.
|
||||
*
|
||||
* If you wanted to access PG_DIAG_MESSAGE_DETAIL you would do the following:
|
||||
* @example ```ts
|
||||
*
|
||||
* console.log(pq.errorFields().messageDetail);
|
||||
* ```
|
||||
* @returns {Libpq.ResultError}
|
||||
*/
|
||||
resultErrorFields(): Libpq.ResultError;
|
||||
|
||||
/**
|
||||
* Retrieves the error message from the result. This will return null if the result does not
|
||||
* have an error.
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
resultErrorMessage(): string;
|
||||
|
||||
/**
|
||||
* @returns {string} either PGRES_COMMAND_OK or PGRES_FATAL_ERROR depending on the status of
|
||||
* the last executed command.
|
||||
*/
|
||||
resultStatus(): string;
|
||||
|
||||
/**
|
||||
* (async) Sends a query to the server to be processed.
|
||||
*
|
||||
* @param {string} [commandText=""] a required string containing the query text.
|
||||
* @returns {boolean} true if the command was sent succesfully or false if it failed to send.
|
||||
*/
|
||||
sendQuery(commandText?: string): boolean;
|
||||
|
||||
/**
|
||||
* (async) Sends a query and to the server to be processed.
|
||||
*
|
||||
* @param {string} [commandText=""] a required string containing the query text.
|
||||
* @param {Array.<(string|number)>} [parameters=[]] an array of parameters as strings used in
|
||||
* the parameterized query.
|
||||
* @returns {boolean} true if the command was sent succesfully or false if it failed to send.
|
||||
*/
|
||||
sendQueryParams(commandText?: string, parameters?: Array<string|number>): boolean;
|
||||
|
||||
/**
|
||||
* (async) Sends a request to the backend to prepare a named statement with the given name.
|
||||
*
|
||||
* @param {string} [statementName=""] a required string of name of the statement to prepare.
|
||||
* @param {string} [commandText=""] a required string of the query.
|
||||
* @param {number} [nParams=0] a count of the number of parameters in the commandText.
|
||||
* @returns {boolean} true if the command was sent succesfully or false if it failed to send.
|
||||
*/
|
||||
sendPrepare(statementName?: string, commandText?: string, nParams?: number): boolean;
|
||||
|
||||
/**
|
||||
* (async) Sends a request to execute a previously prepared statement.
|
||||
*
|
||||
* @param {string} [statementName=""] a required string of the name of the prepared statement.
|
||||
* @param {string[]} [parameters=[]] the parameters to pass to the prepared statement.
|
||||
* @returns {boolean} true if the command was sent succesfully or false if it failed to send.
|
||||
*/
|
||||
sendQueryPrepared(statementName?: string, parameters?: string[]): boolean;
|
||||
|
||||
/**
|
||||
* @returns the version of the connected PostgreSQL backend server as a number.
|
||||
*/
|
||||
serverVersion(): number;
|
||||
|
||||
/**
|
||||
* Toggle the socket blocking on write.
|
||||
*
|
||||
* @param {boolean} [nonBlocking] true to set the connection to use non-blocking writes, false to
|
||||
* use blocking writes.
|
||||
*
|
||||
* @returns {boolean} true if the socket's state was succesfully toggled, false if there was
|
||||
* an error.
|
||||
*/
|
||||
setNonBlocking(nonBlocking?: boolean): boolean;
|
||||
|
||||
/**
|
||||
* @returns {number} an int representing the file descriptor for the socket used internally by
|
||||
* the connection.
|
||||
*/
|
||||
socket(): number;
|
||||
|
||||
/**
|
||||
* This uses libuv to start a read watcher on the socket open to the backend. As soon as this
|
||||
* socket becomes readable the pq instance will emit a readable event. It is up to you to call
|
||||
* [[Libpq.consumeInput]] one or more times to clear this read notification or it will
|
||||
* continue to emit read events over and over and over. The exact flow is outlined [here] under
|
||||
* the documentation for PQisBusy.
|
||||
*/
|
||||
startReader(): void;
|
||||
|
||||
/**
|
||||
* Tells libuv to stop the read watcher on the connection socket.
|
||||
*/
|
||||
stopReader(): void;
|
||||
|
||||
/**
|
||||
* Call this to make sure the socket has flushed all data to the operating system. Once the
|
||||
* socket is writable, your callback will be called. Usefully when using PQsetNonBlocking
|
||||
* and PQflush for async writing.
|
||||
*
|
||||
* @param {Function} callback
|
||||
*/
|
||||
writable(callback: () => void): void;
|
||||
}
|
||||
|
||||
export = Libpq;
|
||||
828
libpq/libpq-tests.ts
Normal file
828
libpq/libpq-tests.ts
Normal file
@@ -0,0 +1,828 @@
|
||||
/// <reference types="node" />
|
||||
/// <reference types="mocha" />
|
||||
|
||||
import {Buffer} from 'buffer';
|
||||
import * as assert from 'assert';
|
||||
import * as async from 'async';
|
||||
import * as PQ from 'libpq';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
declare const ok: Function;
|
||||
|
||||
const createTable = (pq: PQ) => {
|
||||
pq.exec('CREATE TEMP TABLE test_data(name text, age int)');
|
||||
console.log(pq.resultErrorMessage());
|
||||
pq.exec("INSERT INTO test_data(name, age) VALUES ('brian', 32), ('aaron', 30), ('', null);")
|
||||
};
|
||||
|
||||
const blink = (n: number, cb: Function) => {
|
||||
const connections: PQ[] = [];
|
||||
for (let i = 0; i < 30; i++) {
|
||||
connections.push(new PQ())
|
||||
}
|
||||
const connect = (con: PQ, cb: (err?: Error) => void) => {
|
||||
con.connect(cb);
|
||||
};
|
||||
async.each(connections, connect, ok(() => {
|
||||
connections.forEach((con) => {
|
||||
con.finish();
|
||||
});
|
||||
cb();
|
||||
}))
|
||||
};
|
||||
|
||||
const queryText = "SELECT * FROM generate_series(1, 1000)";
|
||||
|
||||
const query = (pq: PQ, cb: Function) => {
|
||||
const readError = (message?: string) => {
|
||||
cleanup();
|
||||
return cb(new Error(message || pq.errorMessage()));
|
||||
};
|
||||
|
||||
const onReadable = () => {
|
||||
if (!pq.consumeInput()) {
|
||||
return readError();
|
||||
}
|
||||
|
||||
if (pq.isBusy()) {
|
||||
return;
|
||||
}
|
||||
|
||||
pq.getResult();
|
||||
|
||||
if (pq.getResult()) {
|
||||
return readError('Only one result at a time is accepted');
|
||||
}
|
||||
cleanup();
|
||||
return cb(null, [])
|
||||
};
|
||||
|
||||
const sent = pq.sendQuery(queryText);
|
||||
if (!sent) return cb(new Error(pq.errorMessage()));
|
||||
console.log('sent query');
|
||||
|
||||
const cleanup = () => {
|
||||
pq.removeListener('readable', onReadable);
|
||||
pq.stopReader();
|
||||
};
|
||||
|
||||
pq.on('readable', onReadable);
|
||||
pq.startReader();
|
||||
};
|
||||
|
||||
describe('async connection', () => {
|
||||
it('works', (done) => {
|
||||
const pq = new PQ();
|
||||
assert(!pq.connected, 'should have connected set to falsy');
|
||||
pq.connect(err => {
|
||||
assert.ifError(err);
|
||||
pq.exec('SELECT NOW()');
|
||||
assert.equal(pq.ntuples(), 1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('works with hard-coded connection parameters', (done) => {
|
||||
const pq = new PQ();
|
||||
const conString = `host=${process.env.PGHOST || 'localhost'}`;
|
||||
pq.connect(conString, done);
|
||||
});
|
||||
|
||||
it('returns an error to the callback if connection fails', (done) => {
|
||||
new PQ().connect('host=asldkfjasldkfjalskdfjasdf', err => {
|
||||
assert(err, 'should have passed an error');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('respects the active domain', (done) => {
|
||||
const pq = new PQ();
|
||||
const domain = require('domain').create();
|
||||
domain.run(() => {
|
||||
const activeDomain = process.domain;
|
||||
assert(activeDomain, 'Should have an active domain');
|
||||
pq.connect(() => {
|
||||
assert.strictEqual(process.domain, activeDomain, 'Active domain is lost');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const consume = (pq: PQ, cb: Function) => {
|
||||
if (!pq.isBusy()) return cb();
|
||||
pq.startReader();
|
||||
const onReadable = () => {
|
||||
assert(pq.consumeInput(), pq.errorMessage());
|
||||
if (pq.isBusy()) {
|
||||
console.log('consuming a 2nd buffer of input later...');
|
||||
return;
|
||||
}
|
||||
pq.removeListener('readable', onReadable);
|
||||
pq.stopReader();
|
||||
cb();
|
||||
};
|
||||
pq.on('readable', onReadable);
|
||||
};
|
||||
|
||||
describe('async simple query', () => {
|
||||
let pq: PQ;
|
||||
|
||||
it('dispatches simple query', (done: Function) => {
|
||||
assert(pq.setNonBlocking(true));
|
||||
pq.writable(() => {
|
||||
const success = pq.sendQuery('SELECT 1');
|
||||
assert.strictEqual(pq.flush(), 0, 'Should have flushed all data to socket');
|
||||
assert(success, pq.errorMessage());
|
||||
consume(pq, () => {
|
||||
assert.ifError(pq.errorMessage());
|
||||
assert(pq.getResult());
|
||||
assert.strictEqual(pq.getResult(), false);
|
||||
assert.strictEqual(pq.ntuples(), 1);
|
||||
assert.strictEqual(pq.getvalue(0, 0), '1');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('dispatches parameterized query', (done: Function) => {
|
||||
const success = pq.sendQueryParams('SELECT $1::text as name', ['Brian']);
|
||||
assert(success, pq.errorMessage());
|
||||
assert.strictEqual(pq.flush(), 0, 'Should have flushed query text & parameters');
|
||||
consume(pq, () => {
|
||||
assert.ifError(pq.errorMessage());
|
||||
assert(pq.getResult());
|
||||
assert.strictEqual(pq.getResult(), false);
|
||||
assert.strictEqual(pq.ntuples(), 1);
|
||||
assert.equal(pq.getvalue(0, 0), 'Brian');
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it('dispatches named query', (done: Function) => {
|
||||
const statementName = 'async-get-name';
|
||||
const success = pq.sendPrepare(statementName, 'SELECT $1::text as name', 1);
|
||||
assert(success, pq.errorMessage());
|
||||
assert.strictEqual(pq.flush(), 0, 'Should have flushed query text');
|
||||
consume(pq, () => {
|
||||
assert.ifError(pq.errorMessage());
|
||||
|
||||
//first time there should be a result
|
||||
assert(pq.getResult());
|
||||
|
||||
//call 'getResult' until it returns false indicating
|
||||
//there is no more input to consume
|
||||
assert.strictEqual(pq.getResult(), false);
|
||||
|
||||
//since we only prepared a statement there should be
|
||||
//0 tuples in the result
|
||||
assert.equal(pq.ntuples(), 0);
|
||||
|
||||
//now execute the previously prepared statement
|
||||
const success = pq.sendQueryPrepared(statementName, ['Brian']);
|
||||
assert(success, pq.errorMessage());
|
||||
assert.strictEqual(pq.flush(), 0, 'Should have flushed parameters');
|
||||
consume(pq, () => {
|
||||
assert.ifError(pq.errorMessage());
|
||||
|
||||
//consume the result of the query execution
|
||||
assert(pq.getResult());
|
||||
assert.equal(pq.ntuples(), 1);
|
||||
assert.equal(pq.getvalue(0, 0), 'Brian');
|
||||
|
||||
//call 'getResult' again to ensure we're finished
|
||||
assert.strictEqual(pq.getResult(), false);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('cancel a request', () => {
|
||||
it('works', (done) => {
|
||||
const pq = new PQ();
|
||||
pq.connectSync();
|
||||
const sent = pq.sendQuery('pg_sleep(5000)');
|
||||
assert(sent, 'should have sent');
|
||||
const canceled = pq.cancel();
|
||||
assert.strictEqual(canceled, true, 'should have canceled');
|
||||
const hasResult = pq.getResult();
|
||||
assert(hasResult, 'should have a result');
|
||||
assert.equal(pq.resultStatus(), 'PGRES_FATAL_ERROR');
|
||||
assert.equal(pq.getResult(), false);
|
||||
pq.exec('SELECT NOW()');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Constructing multiple', () => {
|
||||
it('works all at once', () => {
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
const pq = new PQ();
|
||||
}
|
||||
});
|
||||
|
||||
it('connects and disconnects each client', (done) => {
|
||||
const connect = (n: number, cb: (err?: Error) => void) => {
|
||||
const pq = new PQ();
|
||||
pq.connect(cb);
|
||||
};
|
||||
async.times(30, connect, done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('COPY IN', () => {
|
||||
let pq: PQ;
|
||||
|
||||
before(() => {
|
||||
pq = new PQ();
|
||||
pq.connectSync();
|
||||
createTable(pq);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
pq.finish();
|
||||
});
|
||||
|
||||
it('check existing data assuptions', () => {
|
||||
pq.exec('SELECT COUNT(*) FROM test_data');
|
||||
assert.equal(pq.getvalue(0, 0), 3);
|
||||
});
|
||||
|
||||
it('copies data in', () => {
|
||||
const success = pq.exec('COPY test_data FROM stdin');
|
||||
assert.equal(pq.resultStatus(), 'PGRES_COPY_IN');
|
||||
|
||||
const buffer = new Buffer("bob\t100\n", 'utf8');
|
||||
const res1 = pq.putCopyData(buffer);
|
||||
assert.strictEqual(res1, 1);
|
||||
|
||||
const res2 = pq.putCopyEnd();
|
||||
assert.strictEqual(res2, 1);
|
||||
|
||||
while (pq.getResult()) {
|
||||
}
|
||||
|
||||
pq.exec('SELECT COUNT(*) FROM test_data');
|
||||
assert.equal(pq.getvalue(0, 0), 4);
|
||||
});
|
||||
|
||||
it('can cancel copy data in', () => {
|
||||
const success = pq.exec('COPY test_data FROM stdin');
|
||||
assert.equal(pq.resultStatus(), 'PGRES_COPY_IN');
|
||||
|
||||
const buffer = new Buffer("bob\t100\n", 'utf8');
|
||||
const res1 = pq.putCopyData(buffer);
|
||||
assert.strictEqual(res1, 1);
|
||||
|
||||
const res2 = pq.putCopyEnd('cancel!');
|
||||
assert.strictEqual(res2, 1);
|
||||
|
||||
while (pq.getResult()) {
|
||||
}
|
||||
assert(pq.errorMessage());
|
||||
assert(
|
||||
pq.errorMessage().includes('cancel!'),
|
||||
`${pq.errorMessage()} should have contained "cancel!"`
|
||||
);
|
||||
|
||||
pq.exec('SELECT COUNT(*) FROM test_data');
|
||||
assert.equal(pq.getvalue(0, 0), 4);
|
||||
});
|
||||
});
|
||||
|
||||
describe('COPY OUT', () => {
|
||||
let pq: PQ;
|
||||
|
||||
before(() => {
|
||||
pq = new PQ();
|
||||
pq.connectSync();
|
||||
createTable(pq);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
pq.finish();
|
||||
});
|
||||
|
||||
const getRow = (pq: PQ, expected: string) => {
|
||||
const result = <Buffer> pq.getCopyData(false);
|
||||
assert(result instanceof Buffer, 'Result should be a buffer');
|
||||
assert.equal(result.toString('utf8'), expected);
|
||||
};
|
||||
|
||||
it('copies data out', () => {
|
||||
pq.exec('COPY test_data TO stdin');
|
||||
assert.equal(pq.resultStatus(), 'PGRES_COPY_OUT');
|
||||
getRow(pq, 'brian\t32\n');
|
||||
getRow(pq, 'aaron\t30\n');
|
||||
getRow(pq, '\t\\N\n');
|
||||
assert.strictEqual(<number> pq.getCopyData(), -1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('without being connected', () => {
|
||||
it('exec fails', () => {
|
||||
const pq = new PQ();
|
||||
pq.exec();
|
||||
assert.equal(pq.resultStatus(), 'PGRES_FATAL_ERROR');
|
||||
assert(pq.errorMessage());
|
||||
});
|
||||
|
||||
it('fails on async query', () => {
|
||||
const pq = new PQ();
|
||||
const success = pq.sendQuery('blah');
|
||||
assert.strictEqual(success, false);
|
||||
assert.equal(pq.resultStatus(), 'PGRES_FATAL_ERROR');
|
||||
assert(pq.errorMessage());
|
||||
});
|
||||
|
||||
it('throws when reading while not connected', () => {
|
||||
const pq = new PQ();
|
||||
assert.throws(() => {
|
||||
pq.startReader();
|
||||
});
|
||||
});
|
||||
|
||||
it('throws when writing while not connected', () => {
|
||||
const pq = new PQ();
|
||||
assert.throws(() => {
|
||||
pq.writable(() => {
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('error info', () => {
|
||||
let pq: PQ;
|
||||
|
||||
before(() => {
|
||||
pq = new PQ();
|
||||
pq.connectSync();
|
||||
createTable(pq);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
pq.finish();
|
||||
});
|
||||
|
||||
describe('when there is no error', () => {
|
||||
|
||||
it('everything is null', () => {
|
||||
pq.exec('SELECT NOW()');
|
||||
assert(!pq.errorMessage(), pq.errorMessage());
|
||||
assert.equal(pq.ntuples(), 1);
|
||||
assert(pq.resultErrorFields(), undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when there is an error', () => {
|
||||
it('sets all error codes', () => {
|
||||
pq.exec('INSERT INTO test_data VALUES(1, NOW())');
|
||||
assert(pq.errorMessage());
|
||||
const err = pq.resultErrorFields();
|
||||
assert.notEqual(err, null);
|
||||
assert.equal(err.severity, 'ERROR');
|
||||
assert.equal(err.sqlState, 42804);
|
||||
assert.equal(err.messagePrimary, 'column "age" is of type integer but expression is of type timestamp with time zone');
|
||||
assert.equal(err.messageDetail, undefined);
|
||||
assert.equal(err.messageHint, 'You will need to rewrite or cast the expression.');
|
||||
assert.equal(err.statementPosition, 33);
|
||||
assert.equal(err.internalPosition, undefined);
|
||||
assert.equal(err.internalQuery, undefined);
|
||||
assert.equal(err.context, undefined);
|
||||
assert.equal(err.schemaName, undefined);
|
||||
assert.equal(err.tableName, undefined);
|
||||
assert.equal(err.dataTypeName, undefined);
|
||||
assert.equal(err.constraintName, undefined);
|
||||
assert.equal(err.sourceFile, "parse_target.c");
|
||||
assert(parseInt(err.sourceLine));
|
||||
assert.equal(err.sourceFunction, "transformAssignedExpr");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('escapeLiteral', () => {
|
||||
it('fails to escape when the server is not connected', () => {
|
||||
const pq = new PQ();
|
||||
const result = pq.escapeLiteral('test');
|
||||
assert.strictEqual(result, null);
|
||||
assert(pq.errorMessage());
|
||||
});
|
||||
|
||||
it('escapes a simple string', () => {
|
||||
const pq = new PQ();
|
||||
pq.connectSync();
|
||||
const result = pq.escapeLiteral('bang');
|
||||
assert.equal(result, "'bang'");
|
||||
});
|
||||
|
||||
it('escapes a bad string', () => {
|
||||
const pq = new PQ();
|
||||
pq.connectSync();
|
||||
const result = pq.escapeLiteral("'; TRUNCATE TABLE blah;");
|
||||
assert.equal(result, "'''; TRUNCATE TABLE blah;'");
|
||||
});
|
||||
});
|
||||
|
||||
describe('escapeIdentifier', () => {
|
||||
it('fails when the server is not connected', () => {
|
||||
const pq = new PQ();
|
||||
const result = pq.escapeIdentifier('test');
|
||||
assert.strictEqual(result, null);
|
||||
assert(pq.errorMessage());
|
||||
});
|
||||
|
||||
it('escapes a simple string', () => {
|
||||
const pq = new PQ();
|
||||
pq.connectSync();
|
||||
const result = pq.escapeIdentifier('bang');
|
||||
assert.equal(result, '"bang"');
|
||||
});
|
||||
});
|
||||
|
||||
describe('connecting', () => {
|
||||
it('works', () => {
|
||||
const client = new PQ();
|
||||
client.connectSync();
|
||||
});
|
||||
});
|
||||
|
||||
describe('many connections', () => {
|
||||
it('works', (done) => {
|
||||
async.timesSeries(10, blink, done)
|
||||
})
|
||||
});
|
||||
|
||||
describe('connectSync', () => {
|
||||
it('works 50 times in a row', () => {
|
||||
const pqs = _.times(50, () => new PQ());
|
||||
pqs.forEach((pq) => {
|
||||
pq.connectSync();
|
||||
});
|
||||
pqs.forEach((pq) => {
|
||||
pq.finish();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('connect async', () => {
|
||||
const total = 50;
|
||||
it(`works ${total} times in a row`, (done) => {
|
||||
const pqs = _.times(total, () => new PQ());
|
||||
|
||||
let count = 0;
|
||||
const connect = (cb: Function) => {
|
||||
pqs.forEach((pq) => {
|
||||
pq.connect((err) => {
|
||||
assert.ifError(err);
|
||||
count++;
|
||||
pq.startReader();
|
||||
if (count == total) {
|
||||
cb();
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
connect(() => {
|
||||
pqs.forEach((pq) => {
|
||||
pq.stopReader();
|
||||
pq.finish();
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('multiple queries', () => {
|
||||
const pq = new PQ();
|
||||
|
||||
before((done) => {
|
||||
pq.connect(done)
|
||||
});
|
||||
|
||||
it('first query works', (done) => {
|
||||
query(pq, done);
|
||||
});
|
||||
|
||||
it('second query works', (done) => {
|
||||
query(pq, done);
|
||||
});
|
||||
|
||||
it('third query works', (done) => {
|
||||
query(pq, done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('set & get non blocking', () => {
|
||||
let pq: PQ;
|
||||
|
||||
before(() => {
|
||||
pq = new PQ();
|
||||
pq.connectSync();
|
||||
createTable(pq);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
pq.finish();
|
||||
});
|
||||
|
||||
it('is initially set to false', () => {
|
||||
assert.strictEqual(pq.isNonBlocking(), false);
|
||||
});
|
||||
|
||||
it('can switch back and forth', () => {
|
||||
assert.strictEqual(pq.setNonBlocking(true), true);
|
||||
assert.strictEqual(pq.isNonBlocking(), true);
|
||||
assert.strictEqual(pq.setNonBlocking(), true);
|
||||
assert.strictEqual(pq.isNonBlocking(), false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('LISTEN/NOTIFY', () => {
|
||||
let listener: PQ;
|
||||
let notifier: PQ;
|
||||
|
||||
before(() => {
|
||||
listener = new PQ();
|
||||
notifier = new PQ();
|
||||
listener.connectSync();
|
||||
notifier.connectSync();
|
||||
});
|
||||
|
||||
it('works', () => {
|
||||
notifier.exec("NOTIFY testing, 'My Payload'");
|
||||
let notice = listener.notifies();
|
||||
assert.equal(notice, null);
|
||||
|
||||
listener.exec('LISTEN testing');
|
||||
notifier.exec("NOTIFY testing, 'My Second Payload'");
|
||||
listener.exec('SELECT NOW()');
|
||||
notice = listener.notifies();
|
||||
assert(notice, 'listener should have had a notification come in');
|
||||
assert.equal(notice.relname, 'testing', 'missing relname == testing');
|
||||
assert.equal(notice.extra, 'My Second Payload');
|
||||
assert(notice.be_pid);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
listener.finish();
|
||||
notifier.finish();
|
||||
});
|
||||
});
|
||||
|
||||
describe('result accessors', () => {
|
||||
let pq: PQ;
|
||||
|
||||
before(() => {
|
||||
pq = new PQ();
|
||||
pq.connectSync();
|
||||
createTable(pq);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
pq.finish();
|
||||
});
|
||||
|
||||
before(() => {
|
||||
pq.exec("INSERT INTO test_data(name, age) VALUES ('bob', 80) RETURNING *");
|
||||
assert(!pq.errorMessage());
|
||||
});
|
||||
|
||||
it('has ntuples', () => {
|
||||
assert.strictEqual(pq.ntuples(), 1);
|
||||
});
|
||||
|
||||
it('has cmdStatus', () => {
|
||||
assert.equal(pq.cmdStatus(), 'INSERT 0 1');
|
||||
});
|
||||
|
||||
it('has command tuples', () => {
|
||||
assert.strictEqual(pq.cmdTuples(), '1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Retrieve server version from connection', () => {
|
||||
|
||||
it('return version number when connected', () => {
|
||||
const pq = new PQ();
|
||||
pq.connectSync();
|
||||
const version = pq.serverVersion();
|
||||
assert.equal(typeof version, 'number');
|
||||
assert(version > 60000);
|
||||
});
|
||||
|
||||
it('return zero when not connected', () => {
|
||||
const pq = new PQ();
|
||||
const version = pq.serverVersion();
|
||||
assert.equal(typeof version, 'number');
|
||||
assert.equal(version, 0);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('getting socket', () => {
|
||||
let pq: PQ;
|
||||
|
||||
before(() => {
|
||||
pq = new PQ();
|
||||
pq.connectSync();
|
||||
createTable(pq);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
pq.finish();
|
||||
});
|
||||
|
||||
it('returns -1 when not connected', () => {
|
||||
const pq = new PQ();
|
||||
assert.equal(pq.socket(), -1);
|
||||
});
|
||||
|
||||
it('returns value when connected', () => {
|
||||
assert(pq.socket() > 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('connecting with bad credentials', () => {
|
||||
it('throws an error', () => {
|
||||
try {
|
||||
new PQ().connectSync('asldkfjlasdf');
|
||||
} catch (e) {
|
||||
assert.equal(e.toString().indexOf('connection pointer is NULL'), -1);
|
||||
return;
|
||||
}
|
||||
|
||||
assert.fail(null, null, 'Should have thrown an exception', '');
|
||||
});
|
||||
});
|
||||
|
||||
describe('connecting with no credentials', () => {
|
||||
let pq: PQ;
|
||||
|
||||
before(() => {
|
||||
pq = new PQ();
|
||||
pq.connectSync();
|
||||
});
|
||||
|
||||
it('is connected', () => {
|
||||
assert(pq.connected, 'should have connected == true');
|
||||
});
|
||||
|
||||
after(() => {
|
||||
pq.finish();
|
||||
assert(!pq.connected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('result checking', () => {
|
||||
let pq: PQ;
|
||||
|
||||
before(() => {
|
||||
pq = new PQ();
|
||||
pq.connectSync();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
pq.finish();
|
||||
});
|
||||
|
||||
it('executes query', () => {
|
||||
pq.exec('SELECT NOW() as my_col');
|
||||
assert.equal(pq.resultStatus(), 'PGRES_TUPLES_OK');
|
||||
});
|
||||
|
||||
it('has 1 tuple', () => {
|
||||
assert.equal(pq.ntuples(), 1);
|
||||
});
|
||||
|
||||
it('has 1 field', () => {
|
||||
assert.strictEqual(pq.nfields(), 1);
|
||||
});
|
||||
|
||||
it('has column name', () => {
|
||||
assert.equal(pq.fname(0), 'my_col');
|
||||
});
|
||||
|
||||
it('has oid type of timestamptz', () => {
|
||||
assert.strictEqual(pq.ftype(0), 1184);
|
||||
});
|
||||
|
||||
it('has value as a date', () => {
|
||||
const now = new Date();
|
||||
const val = pq.getvalue(0);
|
||||
const date = new Date(Date.parse(val));
|
||||
assert.equal(date.getFullYear(), now.getFullYear());
|
||||
assert.equal(date.getMonth(), now.getMonth());
|
||||
});
|
||||
|
||||
it('can manually clear result multiple times', () => {
|
||||
pq.clear();
|
||||
pq.clear();
|
||||
pq.clear();
|
||||
});
|
||||
});
|
||||
|
||||
describe('low-level query integration tests', () => {
|
||||
let pq: PQ;
|
||||
|
||||
before(() => {
|
||||
pq = new PQ();
|
||||
pq.connectSync();
|
||||
createTable(pq);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
pq.finish();
|
||||
});
|
||||
|
||||
describe('exec', () => {
|
||||
before(() => {
|
||||
pq.exec('SELECT * FROM test_data');
|
||||
});
|
||||
|
||||
it('has correct tuples', () => {
|
||||
assert.strictEqual(pq.ntuples(), 3);
|
||||
});
|
||||
|
||||
it('has correct field count', () => {
|
||||
assert.strictEqual(pq.nfields(), 2);
|
||||
});
|
||||
|
||||
it('has correct rows', () => {
|
||||
assert.strictEqual(pq.getvalue(0, 0), 'brian');
|
||||
assert.strictEqual(pq.getvalue(1, 1), '30');
|
||||
assert.strictEqual(pq.getvalue(2, 0), '');
|
||||
assert.strictEqual(pq.getisnull(2, 0), false);
|
||||
assert.strictEqual(pq.getvalue(2, 1), '');
|
||||
assert.strictEqual(pq.getisnull(2, 1), true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('sync query with parameters', () => {
|
||||
let pq: PQ;
|
||||
|
||||
before(() => {
|
||||
pq = new PQ();
|
||||
pq.connectSync();
|
||||
createTable(pq);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
pq.finish();
|
||||
});
|
||||
|
||||
it('works with single string parameter', () => {
|
||||
const queryText = 'SELECT $1::text as name';
|
||||
pq.execParams(queryText, ['Brian']);
|
||||
assert.strictEqual(pq.ntuples(), 1);
|
||||
assert.strictEqual(pq.getvalue(0, 0), 'Brian');
|
||||
});
|
||||
|
||||
it('works with a number parameter', () => {
|
||||
const queryText = 'SELECT $1::int as age';
|
||||
pq.execParams(queryText, [32]);
|
||||
assert.strictEqual(pq.ntuples(), 1);
|
||||
assert.strictEqual(pq.getvalue(0, 0), '32');
|
||||
});
|
||||
|
||||
it('works with multiple parameters', () => {
|
||||
const queryText = 'INSERT INTO test_data(name, age) VALUES($1, $2)';
|
||||
pq.execParams(queryText, ['Barkley', 4]);
|
||||
assert.equal(pq.resultErrorMessage(), '');
|
||||
});
|
||||
});
|
||||
|
||||
describe('prepare and execPrepared', () => {
|
||||
let pq: PQ;
|
||||
|
||||
before(() => {
|
||||
pq = new PQ();
|
||||
pq.connectSync();
|
||||
createTable(pq);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
pq.finish();
|
||||
});
|
||||
|
||||
const statementName = 'get-name';
|
||||
|
||||
describe('preparing a statement', () => {
|
||||
it('works properly', () => {
|
||||
pq.prepare(statementName, 'SELECT $1::text as name', 1);
|
||||
assert.ifError(pq.resultErrorMessage());
|
||||
assert.equal(pq.resultStatus(), 'PGRES_COMMAND_OK');
|
||||
});
|
||||
});
|
||||
|
||||
describe('executing a prepared statement', () => {
|
||||
it('works properly', () => {
|
||||
pq.execPrepared(statementName, ['Brian']);
|
||||
assert.ifError(pq.resultErrorMessage());
|
||||
assert.strictEqual(pq.ntuples(), 1);
|
||||
assert.strictEqual(pq.nfields(), 1);
|
||||
assert.strictEqual(pq.getvalue(0, 0), 'Brian');
|
||||
});
|
||||
});
|
||||
});
|
||||
20
libpq/tsconfig.json
Normal file
20
libpq/tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es6",
|
||||
"noImplicitAny": true,
|
||||
"noImplicitThis": true,
|
||||
"strictNullChecks": true,
|
||||
"baseUrl": "../",
|
||||
"typeRoots": [
|
||||
"../"
|
||||
],
|
||||
"types": [],
|
||||
"noEmit": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"files": [
|
||||
"index.d.ts",
|
||||
"libpq-tests.ts"
|
||||
]
|
||||
}
|
||||
1
libpq/tslint.json
Normal file
1
libpq/tslint.json
Normal file
@@ -0,0 +1 @@
|
||||
{ "extends": "../tslint.json" }
|
||||
Reference in New Issue
Block a user