lint pg-protcol

This commit is contained in:
Brian M. Carlson 2020-04-10 10:43:54 -05:00
parent cb928ded2a
commit 6adbcabf50
16 changed files with 811 additions and 802 deletions

View File

@ -2,14 +2,15 @@
"plugins": [
"prettier"
],
"parser": "@typescript-eslint/parser",
"extends": [
"plugin:prettier/recommended"
"plugin:prettier/recommended",
"prettier/@typescript-eslint"
],
"ignorePatterns": [
"**/*.ts",
"node_modules",
"packages/pg",
"packages/pg-protocol",
"packages/pg-protocol/dist/**/*",
"packages/pg-pool"
],
"parserOptions": {

View File

@ -13,10 +13,11 @@
"test": "yarn lerna exec yarn test",
"build": "yarn lerna exec --scope pg-protocol yarn build",
"pretest": "yarn build",
"lint": "yarn lerna exec --parallel yarn lint"
"lint": "eslint '*/**/*.{js,ts,tsx}'"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^2.27.0",
"@typescript-eslint/parser": "^2.27.0",
"eslint": "^6.8.0",
"eslint-config-prettier": "^6.10.1",
"eslint-plugin-node": "^11.1.0",

View File

@ -1,28 +1,28 @@
// file for microbenchmarking
// file for microbenchmarking
import { Writer } from './buffer-writer'
import { serialize } from './index'
import { BufferReader } from './buffer-reader'
import { Writer } from './buffer-writer';
import { serialize } from './index';
import { BufferReader } from './buffer-reader';
const LOOPS = 1000
let count = 0
let start = Date.now()
const writer = new Writer()
const LOOPS = 1000;
let count = 0;
let start = Date.now();
const writer = new Writer();
const reader = new BufferReader()
const buffer = Buffer.from([33, 33, 33, 33, 33, 33, 33, 0])
const reader = new BufferReader();
const buffer = Buffer.from([33, 33, 33, 33, 33, 33, 33, 0]);
const run = () => {
if (count > LOOPS) {
console.log(Date.now() - start)
console.log(Date.now() - start);
return;
}
count++
for(let i = 0; i < LOOPS; i++) {
reader.setBuffer(0, buffer)
reader.cstring()
count++;
for (let i = 0; i < LOOPS; i++) {
reader.setBuffer(0, buffer);
reader.cstring();
}
setImmediate(run)
}
setImmediate(run);
};
run()
run();

View File

@ -6,8 +6,7 @@ export class BufferReader {
// TODO(bmc): support non-utf8 encoding?
private encoding: string = 'utf-8';
constructor(private offset: number = 0) {
}
constructor(private offset: number = 0) {}
public setBuffer(offset: number, buffer: Buffer): void {
this.offset = offset;
@ -40,8 +39,8 @@ export class BufferReader {
public cstring(): string {
const start = this.offset;
let end = start
while(this.buffer[end++] !== 0) { };
let end = start;
while (this.buffer[end++] !== 0) {}
this.offset = end;
return this.buffer.toString(this.encoding, start, end - 1);
}

View File

@ -5,7 +5,7 @@ export class Writer {
private offset: number = 5;
private headerPosition: number = 0;
constructor(private size = 256) {
this.buffer = Buffer.alloc(size)
this.buffer = Buffer.alloc(size);
}
private ensure(size: number): void {
@ -22,28 +22,27 @@ export class Writer {
public addInt32(num: number): Writer {
this.ensure(4);
this.buffer[this.offset++] = (num >>> 24 & 0xFF);
this.buffer[this.offset++] = (num >>> 16 & 0xFF);
this.buffer[this.offset++] = (num >>> 8 & 0xFF);
this.buffer[this.offset++] = (num >>> 0 & 0xFF);
this.buffer[this.offset++] = (num >>> 24) & 0xff;
this.buffer[this.offset++] = (num >>> 16) & 0xff;
this.buffer[this.offset++] = (num >>> 8) & 0xff;
this.buffer[this.offset++] = (num >>> 0) & 0xff;
return this;
}
public addInt16(num: number): Writer {
this.ensure(2);
this.buffer[this.offset++] = (num >>> 8 & 0xFF);
this.buffer[this.offset++] = (num >>> 0 & 0xFF);
this.buffer[this.offset++] = (num >>> 8) & 0xff;
this.buffer[this.offset++] = (num >>> 0) & 0xff;
return this;
}
public addCString(string: string): Writer {
if (!string) {
this.ensure(1);
} else {
var len = Buffer.byteLength(string);
this.ensure(len + 1); // +1 for null terminator
this.buffer.write(string, this.offset, 'utf-8')
this.buffer.write(string, this.offset, 'utf-8');
this.offset += len;
}
@ -51,7 +50,7 @@ export class Writer {
return this;
}
public addString(string: string = ""): Writer {
public addString(string: string = ''): Writer {
var len = Buffer.byteLength(string);
this.ensure(len);
this.buffer.write(string, this.offset);
@ -70,8 +69,8 @@ export class Writer {
if (code) {
this.buffer[this.headerPosition] = code;
//length is everything in this packet minus the code
const length = this.offset - (this.headerPosition + 1)
this.buffer.writeInt32BE(length, this.headerPosition + 1)
const length = this.offset - (this.headerPosition + 1);
this.buffer.writeInt32BE(length, this.headerPosition + 1);
}
return this.buffer.slice(code ? 0 : 5, this.offset);
}
@ -80,8 +79,7 @@ export class Writer {
var result = this.join(code);
this.offset = 5;
this.headerPosition = 0;
this.buffer = Buffer.allocUnsafe(this.size)
this.buffer = Buffer.allocUnsafe(this.size);
return result;
}
}

View File

@ -1,28 +1,29 @@
import buffers from './testing/test-buffers'
import BufferList from './testing/buffer-list'
import { parse } from '.'
import assert from 'assert'
import { PassThrough } from 'stream'
import { BackendMessage } from './messages'
import buffers from './testing/test-buffers';
import BufferList from './testing/buffer-list';
import { parse } from '.';
import assert from 'assert';
import { PassThrough } from 'stream';
import { BackendMessage } from './messages';
var authOkBuffer = buffers.authenticationOk()
var paramStatusBuffer = buffers.parameterStatus('client_encoding', 'UTF8')
var readyForQueryBuffer = buffers.readyForQuery()
var backendKeyDataBuffer = buffers.backendKeyData(1, 2)
var commandCompleteBuffer = buffers.commandComplete('SELECT 3')
var parseCompleteBuffer = buffers.parseComplete()
var bindCompleteBuffer = buffers.bindComplete()
var portalSuspendedBuffer = buffers.portalSuspended()
var authOkBuffer = buffers.authenticationOk();
var paramStatusBuffer = buffers.parameterStatus('client_encoding', 'UTF8');
var readyForQueryBuffer = buffers.readyForQuery();
var backendKeyDataBuffer = buffers.backendKeyData(1, 2);
var commandCompleteBuffer = buffers.commandComplete('SELECT 3');
var parseCompleteBuffer = buffers.parseComplete();
var bindCompleteBuffer = buffers.bindComplete();
var portalSuspendedBuffer = buffers.portalSuspended();
var addRow = function (bufferList: BufferList, name: string, offset: number) {
return bufferList.addCString(name) // field name
return bufferList
.addCString(name) // field name
.addInt32(offset++) // table id
.addInt16(offset++) // attribute of column number
.addInt32(offset++) // objectId of field's data type
.addInt16(offset++) // datatype size
.addInt32(offset++) // type modifier
.addInt16(0) // format code, 0 => text
}
.addInt16(0); // format code, 0 => text
};
var row1 = {
name: 'id',
@ -31,274 +32,291 @@ var row1 = {
dataTypeID: 3,
dataTypeSize: 4,
typeModifier: 5,
formatCode: 0
}
var oneRowDescBuff = buffers.rowDescription([row1])
row1.name = 'bang'
formatCode: 0,
};
var oneRowDescBuff = buffers.rowDescription([row1]);
row1.name = 'bang';
var twoRowBuf = buffers.rowDescription([row1, {
name: 'whoah',
tableID: 10,
attributeNumber: 11,
dataTypeID: 12,
dataTypeSize: 13,
typeModifier: 14,
formatCode: 0
}])
var twoRowBuf = buffers.rowDescription([
row1,
{
name: 'whoah',
tableID: 10,
attributeNumber: 11,
dataTypeID: 12,
dataTypeSize: 13,
typeModifier: 14,
formatCode: 0,
},
]);
var emptyRowFieldBuf = new BufferList()
.addInt16(0)
.join(true, 'D')
var emptyRowFieldBuf = new BufferList().addInt16(0).join(true, 'D');
var emptyRowFieldBuf = buffers.dataRow([])
var emptyRowFieldBuf = buffers.dataRow([]);
var oneFieldBuf = new BufferList()
.addInt16(1) // number of fields
.addInt32(5) // length of bytes of fields
.addCString('test')
.join(true, 'D')
.join(true, 'D');
var oneFieldBuf = buffers.dataRow(['test'])
var oneFieldBuf = buffers.dataRow(['test']);
var expectedAuthenticationOkayMessage = {
name: 'authenticationOk',
length: 8
}
length: 8,
};
var expectedParameterStatusMessage = {
name: 'parameterStatus',
parameterName: 'client_encoding',
parameterValue: 'UTF8',
length: 25
}
length: 25,
};
var expectedBackendKeyDataMessage = {
name: 'backendKeyData',
processID: 1,
secretKey: 2
}
secretKey: 2,
};
var expectedReadyForQueryMessage = {
name: 'readyForQuery',
length: 5,
status: 'I'
}
status: 'I',
};
var expectedCommandCompleteMessage = {
name: 'commandComplete',
length: 13,
text: 'SELECT 3'
}
text: 'SELECT 3',
};
var emptyRowDescriptionBuffer = new BufferList()
.addInt16(0) // number of fields
.join(true, 'T')
.join(true, 'T');
var expectedEmptyRowDescriptionMessage = {
name: 'rowDescription',
length: 6,
fieldCount: 0,
fields: [],
}
};
var expectedOneRowMessage = {
name: 'rowDescription',
length: 27,
fieldCount: 1,
fields: [{
name: 'id',
tableID: 1,
columnID: 2,
dataTypeID: 3,
dataTypeSize: 4,
dataTypeModifier: 5,
format: 'text'
}]
}
fields: [
{
name: 'id',
tableID: 1,
columnID: 2,
dataTypeID: 3,
dataTypeSize: 4,
dataTypeModifier: 5,
format: 'text',
},
],
};
var expectedTwoRowMessage = {
name: 'rowDescription',
length: 53,
fieldCount: 2,
fields: [{
name: 'bang',
tableID: 1,
columnID: 2,
dataTypeID: 3,
dataTypeSize: 4,
dataTypeModifier: 5,
format: 'text'
},
{
name: 'whoah',
tableID: 10,
columnID: 11,
dataTypeID: 12,
dataTypeSize: 13,
dataTypeModifier: 14,
format: 'text'
}]
}
fields: [
{
name: 'bang',
tableID: 1,
columnID: 2,
dataTypeID: 3,
dataTypeSize: 4,
dataTypeModifier: 5,
format: 'text',
},
{
name: 'whoah',
tableID: 10,
columnID: 11,
dataTypeID: 12,
dataTypeSize: 13,
dataTypeModifier: 14,
format: 'text',
},
],
};
var testForMessage = function (buffer: Buffer, expectedMessage: any) {
it('recieves and parses ' + expectedMessage.name, async () => {
const messages = await parseBuffers([buffer])
const messages = await parseBuffers([buffer]);
const [lastMessage] = messages;
for (const key in expectedMessage) {
assert.deepEqual((lastMessage as any)[key], expectedMessage[key])
assert.deepEqual((lastMessage as any)[key], expectedMessage[key]);
}
})
}
});
};
var plainPasswordBuffer = buffers.authenticationCleartextPassword()
var md5PasswordBuffer = buffers.authenticationMD5Password()
var SASLBuffer = buffers.authenticationSASL()
var SASLContinueBuffer = buffers.authenticationSASLContinue()
var SASLFinalBuffer = buffers.authenticationSASLFinal()
var plainPasswordBuffer = buffers.authenticationCleartextPassword();
var md5PasswordBuffer = buffers.authenticationMD5Password();
var SASLBuffer = buffers.authenticationSASL();
var SASLContinueBuffer = buffers.authenticationSASLContinue();
var SASLFinalBuffer = buffers.authenticationSASLFinal();
var expectedPlainPasswordMessage = {
name: 'authenticationCleartextPassword'
}
name: 'authenticationCleartextPassword',
};
var expectedMD5PasswordMessage = {
name: 'authenticationMD5Password',
salt: Buffer.from([1, 2, 3, 4])
}
salt: Buffer.from([1, 2, 3, 4]),
};
var expectedSASLMessage = {
name: 'authenticationSASL',
mechanisms: ['SCRAM-SHA-256']
}
mechanisms: ['SCRAM-SHA-256'],
};
var expectedSASLContinueMessage = {
name: 'authenticationSASLContinue',
data: 'data',
}
};
var expectedSASLFinalMessage = {
name: 'authenticationSASLFinal',
data: 'data',
}
};
var notificationResponseBuffer = buffers.notification(4, 'hi', 'boom')
var notificationResponseBuffer = buffers.notification(4, 'hi', 'boom');
var expectedNotificationResponseMessage = {
name: 'notification',
processId: 4,
channel: 'hi',
payload: 'boom'
}
payload: 'boom',
};
const parseBuffers = async (buffers: Buffer[]): Promise<BackendMessage[]> => {
const stream = new PassThrough();
for (const buffer of buffers) {
stream.write(buffer);
}
stream.end()
const msgs: BackendMessage[] = []
await parse(stream, (msg) => msgs.push(msg))
return msgs
}
stream.end();
const msgs: BackendMessage[] = [];
await parse(stream, (msg) => msgs.push(msg));
return msgs;
};
describe('PgPacketStream', function () {
testForMessage(authOkBuffer, expectedAuthenticationOkayMessage)
testForMessage(plainPasswordBuffer, expectedPlainPasswordMessage)
testForMessage(md5PasswordBuffer, expectedMD5PasswordMessage)
testForMessage(SASLBuffer, expectedSASLMessage)
testForMessage(SASLContinueBuffer, expectedSASLContinueMessage)
testForMessage(SASLFinalBuffer, expectedSASLFinalMessage)
testForMessage(authOkBuffer, expectedAuthenticationOkayMessage);
testForMessage(plainPasswordBuffer, expectedPlainPasswordMessage);
testForMessage(md5PasswordBuffer, expectedMD5PasswordMessage);
testForMessage(SASLBuffer, expectedSASLMessage);
testForMessage(SASLContinueBuffer, expectedSASLContinueMessage);
testForMessage(SASLFinalBuffer, expectedSASLFinalMessage);
testForMessage(paramStatusBuffer, expectedParameterStatusMessage)
testForMessage(backendKeyDataBuffer, expectedBackendKeyDataMessage)
testForMessage(readyForQueryBuffer, expectedReadyForQueryMessage)
testForMessage(commandCompleteBuffer, expectedCommandCompleteMessage)
testForMessage(notificationResponseBuffer, expectedNotificationResponseMessage)
testForMessage(paramStatusBuffer, expectedParameterStatusMessage);
testForMessage(backendKeyDataBuffer, expectedBackendKeyDataMessage);
testForMessage(readyForQueryBuffer, expectedReadyForQueryMessage);
testForMessage(commandCompleteBuffer, expectedCommandCompleteMessage);
testForMessage(notificationResponseBuffer, expectedNotificationResponseMessage);
testForMessage(buffers.emptyQuery(), {
name: 'emptyQuery',
length: 4,
})
});
testForMessage(Buffer.from([0x6e, 0, 0, 0, 4]), {
name: 'noData'
})
name: 'noData',
});
describe('rowDescription messages', function () {
testForMessage(emptyRowDescriptionBuffer, expectedEmptyRowDescriptionMessage)
testForMessage(oneRowDescBuff, expectedOneRowMessage)
testForMessage(twoRowBuf, expectedTwoRowMessage)
})
testForMessage(emptyRowDescriptionBuffer, expectedEmptyRowDescriptionMessage);
testForMessage(oneRowDescBuff, expectedOneRowMessage);
testForMessage(twoRowBuf, expectedTwoRowMessage);
});
describe('parsing rows', function () {
describe('parsing empty row', function () {
testForMessage(emptyRowFieldBuf, {
name: 'dataRow',
fieldCount: 0
})
})
fieldCount: 0,
});
});
describe('parsing data row with fields', function () {
testForMessage(oneFieldBuf, {
name: 'dataRow',
fieldCount: 1,
fields: ['test']
})
})
})
fields: ['test'],
});
});
});
describe('notice message', function () {
// this uses the same logic as error message
var buff = buffers.notice([{ type: 'C', value: 'code' }])
var buff = buffers.notice([{ type: 'C', value: 'code' }]);
testForMessage(buff, {
name: 'notice',
code: 'code'
})
})
code: 'code',
});
});
testForMessage(buffers.error([]), {
name: 'error'
})
name: 'error',
});
describe('with all the fields', function () {
var buffer = buffers.error([{
type: 'S',
value: 'ERROR'
}, {
type: 'C',
value: 'code'
}, {
type: 'M',
value: 'message'
}, {
type: 'D',
value: 'details'
}, {
type: 'H',
value: 'hint'
}, {
type: 'P',
value: '100'
}, {
type: 'p',
value: '101'
}, {
type: 'q',
value: 'query'
}, {
type: 'W',
value: 'where'
}, {
type: 'F',
value: 'file'
}, {
type: 'L',
value: 'line'
}, {
type: 'R',
value: 'routine'
}, {
type: 'Z', // ignored
value: 'alsdkf'
}])
var buffer = buffers.error([
{
type: 'S',
value: 'ERROR',
},
{
type: 'C',
value: 'code',
},
{
type: 'M',
value: 'message',
},
{
type: 'D',
value: 'details',
},
{
type: 'H',
value: 'hint',
},
{
type: 'P',
value: '100',
},
{
type: 'p',
value: '101',
},
{
type: 'q',
value: 'query',
},
{
type: 'W',
value: 'where',
},
{
type: 'F',
value: 'file',
},
{
type: 'L',
value: 'line',
},
{
type: 'R',
value: 'routine',
},
{
type: 'Z', // ignored
value: 'alsdkf',
},
]);
testForMessage(buffer, {
name: 'error',
@ -313,184 +331,179 @@ describe('PgPacketStream', function () {
where: 'where',
file: 'file',
line: 'line',
routine: 'routine'
})
})
routine: 'routine',
});
});
testForMessage(parseCompleteBuffer, {
name: 'parseComplete'
})
name: 'parseComplete',
});
testForMessage(bindCompleteBuffer, {
name: 'bindComplete'
})
name: 'bindComplete',
});
testForMessage(bindCompleteBuffer, {
name: 'bindComplete'
})
name: 'bindComplete',
});
testForMessage(buffers.closeComplete(), {
name: 'closeComplete'
})
name: 'closeComplete',
});
describe('parses portal suspended message', function () {
testForMessage(portalSuspendedBuffer, {
name: 'portalSuspended'
})
})
name: 'portalSuspended',
});
});
describe('parses replication start message', function () {
testForMessage(Buffer.from([0x57, 0x00, 0x00, 0x00, 0x04]), {
name: 'replicationStart',
length: 4
})
})
length: 4,
});
});
describe('copy', () => {
testForMessage(buffers.copyIn(0), {
name: 'copyInResponse',
length: 7,
binary: false,
columnTypes: []
})
columnTypes: [],
});
testForMessage(buffers.copyIn(2), {
name: 'copyInResponse',
length: 11,
binary: false,
columnTypes: [0, 1]
})
columnTypes: [0, 1],
});
testForMessage(buffers.copyOut(0), {
name: 'copyOutResponse',
length: 7,
binary: false,
columnTypes: []
})
columnTypes: [],
});
testForMessage(buffers.copyOut(3), {
name: 'copyOutResponse',
length: 13,
binary: false,
columnTypes: [0, 1, 2]
})
columnTypes: [0, 1, 2],
});
testForMessage(buffers.copyDone(), {
name: 'copyDone',
length: 4,
})
});
testForMessage(buffers.copyData(Buffer.from([5, 6, 7])), {
name: 'copyData',
length: 7,
chunk: Buffer.from([5, 6, 7])
})
})
chunk: Buffer.from([5, 6, 7]),
});
});
// since the data message on a stream can randomly divide the incomming
// tcp packets anywhere, we need to make sure we can parse every single
// split on a tcp message
describe('split buffer, single message parsing', function () {
var fullBuffer = buffers.dataRow([null, 'bang', 'zug zug', null, '!'])
var fullBuffer = buffers.dataRow([null, 'bang', 'zug zug', null, '!']);
it('parses when full buffer comes in', async function () {
const messages = await parseBuffers([fullBuffer]);
const message = messages[0] as any
assert.equal(message.fields.length, 5)
assert.equal(message.fields[0], null)
assert.equal(message.fields[1], 'bang')
assert.equal(message.fields[2], 'zug zug')
assert.equal(message.fields[3], null)
assert.equal(message.fields[4], '!')
})
const message = messages[0] as any;
assert.equal(message.fields.length, 5);
assert.equal(message.fields[0], null);
assert.equal(message.fields[1], 'bang');
assert.equal(message.fields[2], 'zug zug');
assert.equal(message.fields[3], null);
assert.equal(message.fields[4], '!');
});
var testMessageRecievedAfterSpiltAt = async function (split: number) {
var firstBuffer = Buffer.alloc(fullBuffer.length - split)
var secondBuffer = Buffer.alloc(fullBuffer.length - firstBuffer.length)
fullBuffer.copy(firstBuffer, 0, 0)
fullBuffer.copy(secondBuffer, 0, firstBuffer.length)
var firstBuffer = Buffer.alloc(fullBuffer.length - split);
var secondBuffer = Buffer.alloc(fullBuffer.length - firstBuffer.length);
fullBuffer.copy(firstBuffer, 0, 0);
fullBuffer.copy(secondBuffer, 0, firstBuffer.length);
const messages = await parseBuffers([fullBuffer]);
const message = messages[0] as any
assert.equal(message.fields.length, 5)
assert.equal(message.fields[0], null)
assert.equal(message.fields[1], 'bang')
assert.equal(message.fields[2], 'zug zug')
assert.equal(message.fields[3], null)
assert.equal(message.fields[4], '!')
}
const message = messages[0] as any;
assert.equal(message.fields.length, 5);
assert.equal(message.fields[0], null);
assert.equal(message.fields[1], 'bang');
assert.equal(message.fields[2], 'zug zug');
assert.equal(message.fields[3], null);
assert.equal(message.fields[4], '!');
};
it('parses when split in the middle', function () {
testMessageRecievedAfterSpiltAt(6)
})
testMessageRecievedAfterSpiltAt(6);
});
it('parses when split at end', function () {
testMessageRecievedAfterSpiltAt(2)
})
testMessageRecievedAfterSpiltAt(2);
});
it('parses when split at beginning', function () {
testMessageRecievedAfterSpiltAt(fullBuffer.length - 2)
testMessageRecievedAfterSpiltAt(fullBuffer.length - 1)
testMessageRecievedAfterSpiltAt(fullBuffer.length - 5)
})
})
testMessageRecievedAfterSpiltAt(fullBuffer.length - 2);
testMessageRecievedAfterSpiltAt(fullBuffer.length - 1);
testMessageRecievedAfterSpiltAt(fullBuffer.length - 5);
});
});
describe('split buffer, multiple message parsing', function () {
var dataRowBuffer = buffers.dataRow(['!'])
var readyForQueryBuffer = buffers.readyForQuery()
var fullBuffer = Buffer.alloc(dataRowBuffer.length + readyForQueryBuffer.length)
dataRowBuffer.copy(fullBuffer, 0, 0)
readyForQueryBuffer.copy(fullBuffer, dataRowBuffer.length, 0)
var dataRowBuffer = buffers.dataRow(['!']);
var readyForQueryBuffer = buffers.readyForQuery();
var fullBuffer = Buffer.alloc(dataRowBuffer.length + readyForQueryBuffer.length);
dataRowBuffer.copy(fullBuffer, 0, 0);
readyForQueryBuffer.copy(fullBuffer, dataRowBuffer.length, 0);
var verifyMessages = function (messages: any[]) {
assert.strictEqual(messages.length, 2)
assert.strictEqual(messages.length, 2);
assert.deepEqual(messages[0], {
name: 'dataRow',
fieldCount: 1,
length: 11,
fields: ['!']
})
assert.equal(messages[0].fields[0], '!')
fields: ['!'],
});
assert.equal(messages[0].fields[0], '!');
assert.deepEqual(messages[1], {
name: 'readyForQuery',
length: 5,
status: 'I'
})
}
status: 'I',
});
};
// sanity check
it('recieves both messages when packet is not split', async function () {
const messages = await parseBuffers([fullBuffer])
verifyMessages(messages)
})
const messages = await parseBuffers([fullBuffer]);
verifyMessages(messages);
});
var splitAndVerifyTwoMessages = async function (split: number) {
var firstBuffer = Buffer.alloc(fullBuffer.length - split)
var secondBuffer = Buffer.alloc(fullBuffer.length - firstBuffer.length)
fullBuffer.copy(firstBuffer, 0, 0)
fullBuffer.copy(secondBuffer, 0, firstBuffer.length)
const messages = await parseBuffers([firstBuffer, secondBuffer])
verifyMessages(messages)
}
var firstBuffer = Buffer.alloc(fullBuffer.length - split);
var secondBuffer = Buffer.alloc(fullBuffer.length - firstBuffer.length);
fullBuffer.copy(firstBuffer, 0, 0);
fullBuffer.copy(secondBuffer, 0, firstBuffer.length);
const messages = await parseBuffers([firstBuffer, secondBuffer]);
verifyMessages(messages);
};
describe('recieves both messages when packet is split', function () {
it('in the middle', function () {
return splitAndVerifyTwoMessages(11)
})
return splitAndVerifyTwoMessages(11);
});
it('at the front', function () {
return Promise.all([
splitAndVerifyTwoMessages(fullBuffer.length - 1),
splitAndVerifyTwoMessages(fullBuffer.length - 4),
splitAndVerifyTwoMessages(fullBuffer.length - 6)
])
})
splitAndVerifyTwoMessages(fullBuffer.length - 6),
]);
});
it('at the end', function () {
return Promise.all([
splitAndVerifyTwoMessages(8),
splitAndVerifyTwoMessages(1)
])
})
})
})
})
return Promise.all([splitAndVerifyTwoMessages(8), splitAndVerifyTwoMessages(1)]);
});
});
});
});

View File

@ -1,11 +1,11 @@
import { BackendMessage } from './messages';
import { serialize } from './serializer';
import { Parser, MessageCallback } from './parser'
import { Parser, MessageCallback } from './parser';
export function parse(stream: NodeJS.ReadableStream, callback: MessageCallback): Promise<void> {
const parser = new Parser()
stream.on('data', (buffer: Buffer) => parser.parse(buffer, callback))
return new Promise((resolve) => stream.on('end', () => resolve()))
const parser = new Parser();
stream.on('data', (buffer: Buffer) => parser.parse(buffer, callback));
return new Promise((resolve) => stream.on('end', () => resolve()));
}
export { serialize };

View File

@ -42,37 +42,37 @@ export const parseComplete: BackendMessage = {
export const bindComplete: BackendMessage = {
name: MessageName.bindComplete,
length: 5,
}
};
export const closeComplete: BackendMessage = {
name: MessageName.closeComplete,
length: 5,
}
};
export const noData: BackendMessage = {
name: MessageName.noData,
length: 5
}
length: 5,
};
export const portalSuspended: BackendMessage = {
name: MessageName.portalSuspended,
length: 5,
}
};
export const replicationStart: BackendMessage = {
name: MessageName.replicationStart,
length: 4,
}
};
export const emptyQuery: BackendMessage = {
name: MessageName.emptyQuery,
length: 4,
}
};
export const copyDone: BackendMessage = {
name: MessageName.copyDone,
length: 4,
}
};
interface NoticeOrError {
message: string | undefined;
@ -112,77 +112,89 @@ export class DatabaseError extends Error implements NoticeOrError {
public line: string | undefined;
public routine: string | undefined;
constructor(message: string, public readonly length: number, public readonly name: MessageName) {
super(message)
super(message);
}
}
export class CopyDataMessage {
public readonly name = MessageName.copyData;
constructor(public readonly length: number, public readonly chunk: Buffer) {
}
constructor(public readonly length: number, public readonly chunk: Buffer) {}
}
export class CopyResponse {
public readonly columnTypes: number[];
constructor(public readonly length: number, public readonly name: MessageName, public readonly binary: boolean, columnCount: number) {
constructor(
public readonly length: number,
public readonly name: MessageName,
public readonly binary: boolean,
columnCount: number
) {
this.columnTypes = new Array(columnCount);
}
}
export class Field {
constructor(public readonly name: string, public readonly tableID: number, public readonly columnID: number, public readonly dataTypeID: number, public readonly dataTypeSize: number, public readonly dataTypeModifier: number, public readonly format: Mode) {
}
constructor(
public readonly name: string,
public readonly tableID: number,
public readonly columnID: number,
public readonly dataTypeID: number,
public readonly dataTypeSize: number,
public readonly dataTypeModifier: number,
public readonly format: Mode
) {}
}
export class RowDescriptionMessage {
public readonly name: MessageName = MessageName.rowDescription;
public readonly fields: Field[];
constructor(public readonly length: number, public readonly fieldCount: number) {
this.fields = new Array(this.fieldCount)
this.fields = new Array(this.fieldCount);
}
}
export class ParameterStatusMessage {
public readonly name: MessageName = MessageName.parameterStatus;
constructor(public readonly length: number, public readonly parameterName: string, public readonly parameterValue: string) {
}
constructor(
public readonly length: number,
public readonly parameterName: string,
public readonly parameterValue: string
) {}
}
export class AuthenticationMD5Password implements BackendMessage {
public readonly name: MessageName = MessageName.authenticationMD5Password;
constructor(public readonly length: number, public readonly salt: Buffer) {
}
constructor(public readonly length: number, public readonly salt: Buffer) {}
}
export class BackendKeyDataMessage {
public readonly name: MessageName = MessageName.backendKeyData;
constructor(public readonly length: number, public readonly processID: number, public readonly secretKey: number) {
}
constructor(public readonly length: number, public readonly processID: number, public readonly secretKey: number) {}
}
export class NotificationResponseMessage {
public readonly name: MessageName = MessageName.notification;
constructor(public readonly length: number, public readonly processId: number, public readonly channel: string, public readonly payload: string) {
}
constructor(
public readonly length: number,
public readonly processId: number,
public readonly channel: string,
public readonly payload: string
) {}
}
export class ReadyForQueryMessage {
public readonly name: MessageName = MessageName.readyForQuery;
constructor(public readonly length: number, public readonly status: string) {
}
constructor(public readonly length: number, public readonly status: string) {}
}
export class CommandCompleteMessage {
public readonly name: MessageName = MessageName.commandComplete
constructor(public readonly length: number, public readonly text: string) {
}
public readonly name: MessageName = MessageName.commandComplete;
constructor(public readonly length: number, public readonly text: string) {}
}
export class DataRowMessage {
public readonly fieldCount: number;
public readonly name: MessageName = MessageName.dataRow
public readonly name: MessageName = MessageName.dataRow;
constructor(public length: number, public fields: any[]) {
this.fieldCount = fields.length;
}

View File

@ -1,85 +1,79 @@
import assert from 'assert'
import { serialize } from './serializer'
import BufferList from './testing/buffer-list'
import assert from 'assert';
import { serialize } from './serializer';
import BufferList from './testing/buffer-list';
describe('serializer', () => {
it('builds startup message', function () {
const actual = serialize.startup({
user: 'brian',
database: 'bang'
})
assert.deepEqual(actual, new BufferList()
.addInt16(3)
.addInt16(0)
.addCString('user')
.addCString('brian')
.addCString('database')
.addCString('bang')
.addCString('client_encoding')
.addCString("'utf-8'")
.addCString('').join(true))
})
database: 'bang',
});
assert.deepEqual(
actual,
new BufferList()
.addInt16(3)
.addInt16(0)
.addCString('user')
.addCString('brian')
.addCString('database')
.addCString('bang')
.addCString('client_encoding')
.addCString("'utf-8'")
.addCString('')
.join(true)
);
});
it('builds password message', function () {
const actual = serialize.password('!')
assert.deepEqual(actual, new BufferList().addCString('!').join(true, 'p'))
})
const actual = serialize.password('!');
assert.deepEqual(actual, new BufferList().addCString('!').join(true, 'p'));
});
it('builds request ssl message', function () {
const actual = serialize.requestSsl()
const expected = new BufferList().addInt32(80877103).join(true)
const actual = serialize.requestSsl();
const expected = new BufferList().addInt32(80877103).join(true);
assert.deepEqual(actual, expected);
})
});
it('builds SASLInitialResponseMessage message', function () {
const actual = serialize.sendSASLInitialResponseMessage('mech', 'data')
assert.deepEqual(actual, new BufferList().addCString('mech').addInt32(4).addString('data').join(true, 'p'))
})
const actual = serialize.sendSASLInitialResponseMessage('mech', 'data');
assert.deepEqual(actual, new BufferList().addCString('mech').addInt32(4).addString('data').join(true, 'p'));
});
it('builds SCRAMClientFinalMessage message', function () {
const actual = serialize.sendSCRAMClientFinalMessage('data')
assert.deepEqual(actual, new BufferList().addString('data').join(true, 'p'))
})
const actual = serialize.sendSCRAMClientFinalMessage('data');
assert.deepEqual(actual, new BufferList().addString('data').join(true, 'p'));
});
it('builds query message', function () {
var txt = 'select * from boom'
const actual = serialize.query(txt)
assert.deepEqual(actual, new BufferList().addCString(txt).join(true, 'Q'))
})
var txt = 'select * from boom';
const actual = serialize.query(txt);
assert.deepEqual(actual, new BufferList().addCString(txt).join(true, 'Q'));
});
describe('parse message', () => {
it('builds parse message', function () {
const actual = serialize.parse({ text: '!' })
var expected = new BufferList()
.addCString('')
.addCString('!')
.addInt16(0).join(true, 'P')
assert.deepEqual(actual, expected)
})
const actual = serialize.parse({ text: '!' });
var expected = new BufferList().addCString('').addCString('!').addInt16(0).join(true, 'P');
assert.deepEqual(actual, expected);
});
it('builds parse message with named query', function () {
const actual = serialize.parse({
name: 'boom',
text: 'select * from boom',
types: []
})
var expected = new BufferList()
.addCString('boom')
.addCString('select * from boom')
.addInt16(0).join(true, 'P')
assert.deepEqual(actual, expected)
})
types: [],
});
var expected = new BufferList().addCString('boom').addCString('select * from boom').addInt16(0).join(true, 'P');
assert.deepEqual(actual, expected);
});
it('with multiple parameters', function () {
const actual = serialize.parse({
name: 'force',
text: 'select * from bang where name = $1',
types: [1, 2, 3, 4]
})
types: [1, 2, 3, 4],
});
var expected = new BufferList()
.addCString('force')
.addCString('select * from bang where name = $1')
@ -87,16 +81,15 @@ describe('serializer', () => {
.addInt32(1)
.addInt32(2)
.addInt32(3)
.addInt32(4).join(true, 'P')
assert.deepEqual(actual, expected)
})
})
.addInt32(4)
.join(true, 'P');
assert.deepEqual(actual, expected);
});
});
describe('bind messages', function () {
it('with no values', function () {
const actual = serialize.bind()
const actual = serialize.bind();
var expectedBuffer = new BufferList()
.addCString('')
@ -104,18 +97,18 @@ describe('serializer', () => {
.addInt16(0)
.addInt16(0)
.addInt16(0)
.join(true, 'B')
assert.deepEqual(actual, expectedBuffer)
})
.join(true, 'B');
assert.deepEqual(actual, expectedBuffer);
});
it('with named statement, portal, and values', function () {
const actual = serialize.bind({
portal: 'bang',
statement: 'woo',
values: ['1', 'hi', null, 'zing']
})
values: ['1', 'hi', null, 'zing'],
});
var expectedBuffer = new BufferList()
.addCString('bang') // portal name
.addCString('bang') // portal name
.addCString('woo') // statement name
.addInt16(0)
.addInt16(4)
@ -127,25 +120,25 @@ describe('serializer', () => {
.addInt32(4)
.add(Buffer.from('zing'))
.addInt16(0)
.join(true, 'B')
assert.deepEqual(actual, expectedBuffer)
})
})
.join(true, 'B');
assert.deepEqual(actual, expectedBuffer);
});
});
it('with named statement, portal, and buffer value', function () {
const actual = serialize.bind({
portal: 'bang',
statement: 'woo',
values: ['1', 'hi', null, Buffer.from('zing', 'utf8')]
})
values: ['1', 'hi', null, Buffer.from('zing', 'utf8')],
});
var expectedBuffer = new BufferList()
.addCString('bang') // portal name
.addCString('bang') // portal name
.addCString('woo') // statement name
.addInt16(4)// value count
.addInt16(0)// string
.addInt16(0)// string
.addInt16(0)// string
.addInt16(1)// binary
.addInt16(4) // value count
.addInt16(0) // string
.addInt16(0) // string
.addInt16(0) // string
.addInt16(1) // binary
.addInt16(4)
.addInt32(1)
.add(Buffer.from('1'))
@ -155,102 +148,96 @@ describe('serializer', () => {
.addInt32(4)
.add(Buffer.from('zing', 'utf-8'))
.addInt16(0)
.join(true, 'B')
assert.deepEqual(actual, expectedBuffer)
})
.join(true, 'B');
assert.deepEqual(actual, expectedBuffer);
});
describe('builds execute message', function () {
it('for unamed portal with no row limit', function () {
const actual = serialize.execute()
var expectedBuffer = new BufferList()
.addCString('')
.addInt32(0)
.join(true, 'E')
assert.deepEqual(actual, expectedBuffer)
})
const actual = serialize.execute();
var expectedBuffer = new BufferList().addCString('').addInt32(0).join(true, 'E');
assert.deepEqual(actual, expectedBuffer);
});
it('for named portal with row limit', function () {
const actual = serialize.execute({
portal: 'my favorite portal',
rows: 100
})
var expectedBuffer = new BufferList()
.addCString('my favorite portal')
.addInt32(100)
.join(true, 'E')
assert.deepEqual(actual, expectedBuffer)
})
})
rows: 100,
});
var expectedBuffer = new BufferList().addCString('my favorite portal').addInt32(100).join(true, 'E');
assert.deepEqual(actual, expectedBuffer);
});
});
it('builds flush command', function () {
const actual = serialize.flush()
var expected = new BufferList().join(true, 'H')
assert.deepEqual(actual, expected)
})
const actual = serialize.flush();
var expected = new BufferList().join(true, 'H');
assert.deepEqual(actual, expected);
});
it('builds sync command', function () {
const actual = serialize.sync()
var expected = new BufferList().join(true, 'S')
assert.deepEqual(actual, expected)
})
const actual = serialize.sync();
var expected = new BufferList().join(true, 'S');
assert.deepEqual(actual, expected);
});
it('builds end command', function () {
const actual = serialize.end()
var expected = Buffer.from([0x58, 0, 0, 0, 4])
assert.deepEqual(actual, expected)
})
const actual = serialize.end();
var expected = Buffer.from([0x58, 0, 0, 0, 4]);
assert.deepEqual(actual, expected);
});
describe('builds describe command', function () {
it('describe statement', function () {
const actual = serialize.describe({ type: 'S', name: 'bang' })
var expected = new BufferList().addChar('S').addCString('bang').join(true, 'D')
assert.deepEqual(actual, expected)
})
const actual = serialize.describe({ type: 'S', name: 'bang' });
var expected = new BufferList().addChar('S').addCString('bang').join(true, 'D');
assert.deepEqual(actual, expected);
});
it('describe unnamed portal', function () {
const actual = serialize.describe({ type: 'P' })
var expected = new BufferList().addChar('P').addCString('').join(true, 'D')
assert.deepEqual(actual, expected)
})
})
const actual = serialize.describe({ type: 'P' });
var expected = new BufferList().addChar('P').addCString('').join(true, 'D');
assert.deepEqual(actual, expected);
});
});
describe('builds close command', function () {
it('describe statement', function () {
const actual = serialize.close({ type: 'S', name: 'bang' })
var expected = new BufferList().addChar('S').addCString('bang').join(true, 'C')
assert.deepEqual(actual, expected)
})
const actual = serialize.close({ type: 'S', name: 'bang' });
var expected = new BufferList().addChar('S').addCString('bang').join(true, 'C');
assert.deepEqual(actual, expected);
});
it('describe unnamed portal', function () {
const actual = serialize.close({ type: 'P' })
var expected = new BufferList().addChar('P').addCString('').join(true, 'C')
assert.deepEqual(actual, expected)
})
})
const actual = serialize.close({ type: 'P' });
var expected = new BufferList().addChar('P').addCString('').join(true, 'C');
assert.deepEqual(actual, expected);
});
});
describe('copy messages', function () {
it('builds copyFromChunk', () => {
const actual = serialize.copyData(Buffer.from([1, 2, 3]))
const expected = new BufferList().add(Buffer.from([1, 2,3 ])).join(true, 'd')
assert.deepEqual(actual, expected)
})
const actual = serialize.copyData(Buffer.from([1, 2, 3]));
const expected = new BufferList().add(Buffer.from([1, 2, 3])).join(true, 'd');
assert.deepEqual(actual, expected);
});
it('builds copy fail', () => {
const actual = serialize.copyFail('err!')
const expected = new BufferList().addCString('err!').join(true, 'f')
assert.deepEqual(actual, expected)
})
const actual = serialize.copyFail('err!');
const expected = new BufferList().addCString('err!').join(true, 'f');
assert.deepEqual(actual, expected);
});
it('builds copy done', () => {
const actual = serialize.copyDone()
const expected = new BufferList().join(true, 'c')
assert.deepEqual(actual, expected)
})
})
const actual = serialize.copyDone();
const expected = new BufferList().join(true, 'c');
assert.deepEqual(actual, expected);
});
});
it('builds cancel message', () => {
const actual = serialize.cancel(3, 4)
const expected = new BufferList().addInt16(1234).addInt16(5678).addInt32(3).addInt32(4).join(true)
assert.deepEqual(actual, expected)
})
})
const actual = serialize.cancel(3, 4);
const expected = new BufferList().addInt16(1234).addInt16(5678).addInt32(3).addInt32(4).join(true);
assert.deepEqual(actual, expected);
});
});

View File

@ -1,7 +1,32 @@
import { TransformOptions } from 'stream';
import { Mode, bindComplete, parseComplete, closeComplete, noData, portalSuspended, copyDone, replicationStart, emptyQuery, ReadyForQueryMessage, CommandCompleteMessage, CopyDataMessage, CopyResponse, NotificationResponseMessage, RowDescriptionMessage, Field, DataRowMessage, ParameterStatusMessage, BackendKeyDataMessage, DatabaseError, BackendMessage, MessageName, AuthenticationMD5Password, NoticeMessage } from './messages';
import {
Mode,
bindComplete,
parseComplete,
closeComplete,
noData,
portalSuspended,
copyDone,
replicationStart,
emptyQuery,
ReadyForQueryMessage,
CommandCompleteMessage,
CopyDataMessage,
CopyResponse,
NotificationResponseMessage,
RowDescriptionMessage,
Field,
DataRowMessage,
ParameterStatusMessage,
BackendKeyDataMessage,
DatabaseError,
BackendMessage,
MessageName,
AuthenticationMD5Password,
NoticeMessage,
} from './messages';
import { BufferReader } from './buffer-reader';
import assert from 'assert'
import assert from 'assert';
// every message is prefixed with a single bye
const CODE_LENGTH = 1;
@ -14,13 +39,13 @@ const HEADER_LENGTH = CODE_LENGTH + LEN_LENGTH;
export type Packet = {
code: number;
packet: Buffer;
}
};
const emptyBuffer = Buffer.allocUnsafe(0);
type StreamOptions = TransformOptions & {
mode: Mode
}
mode: Mode;
};
const enum MessageCodes {
DataRow = 0x44, // D
@ -55,7 +80,7 @@ export class Parser {
constructor(opts?: StreamOptions) {
if (opts?.mode === 'binary') {
throw new Error('Binary mode not supported yet')
throw new Error('Binary mode not supported yet');
}
this.mode = opts?.mode || 'text';
}
@ -64,11 +89,11 @@ export class Parser {
let combinedBuffer = buffer;
if (this.remainingBuffer.byteLength) {
combinedBuffer = Buffer.allocUnsafe(this.remainingBuffer.byteLength + buffer.byteLength);
this.remainingBuffer.copy(combinedBuffer)
buffer.copy(combinedBuffer, this.remainingBuffer.byteLength)
this.remainingBuffer.copy(combinedBuffer);
buffer.copy(combinedBuffer, this.remainingBuffer.byteLength);
}
let offset = 0;
while ((offset + HEADER_LENGTH) <= combinedBuffer.byteLength) {
while (offset + HEADER_LENGTH <= combinedBuffer.byteLength) {
// code is 1 byte long - it identifies the message type
const code = combinedBuffer[offset];
@ -79,7 +104,7 @@ export class Parser {
if (fullMessageLength + offset <= combinedBuffer.byteLength) {
const message = this.handlePacket(offset + HEADER_LENGTH, code, length, combinedBuffer);
callback(message)
callback(message);
offset += fullMessageLength;
} else {
break;
@ -89,9 +114,8 @@ export class Parser {
if (offset === combinedBuffer.byteLength) {
this.remainingBuffer = emptyBuffer;
} else {
this.remainingBuffer = combinedBuffer.slice(offset)
this.remainingBuffer = combinedBuffer.slice(offset);
}
}
private handlePacket(offset: number, code: number, length: number, bytes: Buffer): BackendMessage {
@ -139,14 +163,14 @@ export class Parser {
case MessageCodes.CopyData:
return this.parseCopyData(offset, length, bytes);
default:
assert.fail(`unknown message code: ${code.toString(16)}`)
assert.fail(`unknown message code: ${code.toString(16)}`);
}
}
private parseReadyForQueryMessage(offset: number, length: number, bytes: Buffer) {
this.reader.setBuffer(offset, bytes);
const status = this.reader.string(1);
return new ReadyForQueryMessage(length, status)
return new ReadyForQueryMessage(length, status);
}
private parseCommandCompleteMessage(offset: number, length: number, bytes: Buffer) {
@ -161,17 +185,17 @@ export class Parser {
}
private parseCopyInMessage(offset: number, length: number, bytes: Buffer) {
return this.parseCopyMessage(offset, length, bytes, MessageName.copyInResponse)
return this.parseCopyMessage(offset, length, bytes, MessageName.copyInResponse);
}
private parseCopyOutMessage(offset: number, length: number, bytes: Buffer) {
return this.parseCopyMessage(offset, length, bytes, MessageName.copyOutResponse)
return this.parseCopyMessage(offset, length, bytes, MessageName.copyOutResponse);
}
private parseCopyMessage(offset: number, length: number, bytes: Buffer, messageName: MessageName) {
this.reader.setBuffer(offset, bytes);
const isBinary = this.reader.byte() !== 0;
const columnCount = this.reader.int16()
const columnCount = this.reader.int16();
const message = new CopyResponse(length, messageName, isBinary, columnCount);
for (let i = 0; i < columnCount; i++) {
message.columnTypes[i] = this.reader.int16();
@ -189,23 +213,23 @@ export class Parser {
private parseRowDescriptionMessage(offset: number, length: number, bytes: Buffer) {
this.reader.setBuffer(offset, bytes);
const fieldCount = this.reader.int16()
const fieldCount = this.reader.int16();
const message = new RowDescriptionMessage(length, fieldCount);
for (let i = 0; i < fieldCount; i++) {
message.fields[i] = this.parseField()
message.fields[i] = this.parseField();
}
return message;
}
private parseField(): Field {
const name = this.reader.cstring()
const tableID = this.reader.int32()
const columnID = this.reader.int16()
const dataTypeID = this.reader.int32()
const dataTypeSize = this.reader.int16()
const dataTypeModifier = this.reader.int32()
const name = this.reader.cstring();
const tableID = this.reader.int32();
const columnID = this.reader.int16();
const dataTypeID = this.reader.int32();
const dataTypeSize = this.reader.int16();
const dataTypeModifier = this.reader.int32();
const mode = this.reader.int16() === 0 ? 'text' : 'binary';
return new Field(name, tableID, columnID, dataTypeID, dataTypeSize, dataTypeModifier, mode)
return new Field(name, tableID, columnID, dataTypeID, dataTypeSize, dataTypeModifier, mode);
}
private parseDataRowMessage(offset: number, length: number, bytes: Buffer) {
@ -215,7 +239,7 @@ export class Parser {
for (let i = 0; i < fieldCount; i++) {
const len = this.reader.int32();
// a -1 for length means the value of the field is null
fields[i] = len === -1 ? null : this.reader.string(len)
fields[i] = len === -1 ? null : this.reader.string(len);
}
return new DataRowMessage(length, fields);
}
@ -223,21 +247,20 @@ export class Parser {
private parseParameterStatusMessage(offset: number, length: number, bytes: Buffer) {
this.reader.setBuffer(offset, bytes);
const name = this.reader.cstring();
const value = this.reader.cstring()
return new ParameterStatusMessage(length, name, value)
const value = this.reader.cstring();
return new ParameterStatusMessage(length, name, value);
}
private parseBackendKeyData(offset: number, length: number, bytes: Buffer) {
this.reader.setBuffer(offset, bytes);
const processID = this.reader.int32()
const secretKey = this.reader.int32()
return new BackendKeyDataMessage(length, processID, secretKey)
const processID = this.reader.int32();
const secretKey = this.reader.int32();
return new BackendKeyDataMessage(length, processID, secretKey);
}
public parseAuthenticationResponse(offset: number, length: number, bytes: Buffer) {
this.reader.setBuffer(offset, bytes);
const code = this.reader.int32()
const code = this.reader.int32();
// TODO(bmc): maybe better types here
const message: BackendMessage & any = {
name: MessageName.authenticationOk,
@ -249,71 +272,74 @@ export class Parser {
break;
case 3: // AuthenticationCleartextPassword
if (message.length === 8) {
message.name = MessageName.authenticationCleartextPassword
message.name = MessageName.authenticationCleartextPassword;
}
break
break;
case 5: // AuthenticationMD5Password
if (message.length === 12) {
message.name = MessageName.authenticationMD5Password
message.name = MessageName.authenticationMD5Password;
const salt = this.reader.bytes(4);
return new AuthenticationMD5Password(length, salt);
}
break
break;
case 10: // AuthenticationSASL
message.name = MessageName.authenticationSASL
message.mechanisms = []
message.name = MessageName.authenticationSASL;
message.mechanisms = [];
let mechanism: string;
do {
mechanism = this.reader.cstring()
mechanism = this.reader.cstring();
if (mechanism) {
message.mechanisms.push(mechanism)
message.mechanisms.push(mechanism);
}
} while (mechanism)
} while (mechanism);
break;
case 11: // AuthenticationSASLContinue
message.name = MessageName.authenticationSASLContinue
message.data = this.reader.string(length - 4)
message.name = MessageName.authenticationSASLContinue;
message.data = this.reader.string(length - 4);
break;
case 12: // AuthenticationSASLFinal
message.name = MessageName.authenticationSASLFinal
message.data = this.reader.string(length - 4)
message.name = MessageName.authenticationSASLFinal;
message.data = this.reader.string(length - 4);
break;
default:
throw new Error('Unknown authenticationOk message type ' + code)
throw new Error('Unknown authenticationOk message type ' + code);
}
return message;
}
private parseErrorMessage(offset: number, length: number, bytes: Buffer, name: MessageName) {
this.reader.setBuffer(offset, bytes);
const fields: Record<string, string> = {}
let fieldType = this.reader.string(1)
const fields: Record<string, string> = {};
let fieldType = this.reader.string(1);
while (fieldType !== '\0') {
fields[fieldType] = this.reader.cstring()
fieldType = this.reader.string(1)
fields[fieldType] = this.reader.cstring();
fieldType = this.reader.string(1);
}
const messageValue = fields.M
const messageValue = fields.M;
const message = name === MessageName.notice ? new NoticeMessage(length, messageValue) : new DatabaseError(messageValue, length, name)
const message =
name === MessageName.notice
? new NoticeMessage(length, messageValue)
: new DatabaseError(messageValue, length, name);
message.severity = fields.S
message.code = fields.C
message.detail = fields.D
message.hint = fields.H
message.position = fields.P
message.internalPosition = fields.p
message.internalQuery = fields.q
message.where = fields.W
message.schema = fields.s
message.table = fields.t
message.column = fields.c
message.dataType = fields.d
message.constraint = fields.n
message.file = fields.F
message.line = fields.L
message.routine = fields.R
message.severity = fields.S;
message.code = fields.C;
message.detail = fields.D;
message.hint = fields.H;
message.position = fields.P;
message.internalPosition = fields.p;
message.internalQuery = fields.q;
message.where = fields.W;
message.schema = fields.s;
message.table = fields.t;
message.column = fields.c;
message.dataType = fields.d;
message.constraint = fields.n;
message.file = fields.F;
message.line = fields.L;
message.routine = fields.R;
return message;
}
}

View File

@ -1,4 +1,4 @@
import { Writer } from './buffer-writer'
import { Writer } from './buffer-writer';
const enum code {
startup = 0x70,
@ -13,67 +13,61 @@ const enum code {
describe = 0x44,
copyFromChunk = 0x64,
copyDone = 0x63,
copyFail = 0x66
copyFail = 0x66,
}
const writer = new Writer()
const writer = new Writer();
const startup = (opts: Record<string, string>): Buffer => {
// protocol version
writer.addInt16(3).addInt16(0)
writer.addInt16(3).addInt16(0);
for (const key of Object.keys(opts)) {
writer.addCString(key).addCString(opts[key])
writer.addCString(key).addCString(opts[key]);
}
writer.addCString('client_encoding').addCString("'utf-8'")
writer.addCString('client_encoding').addCString("'utf-8'");
var bodyBuffer = writer.addCString('').flush()
var bodyBuffer = writer.addCString('').flush();
// this message is sent without a code
var length = bodyBuffer.length + 4
var length = bodyBuffer.length + 4;
return new Writer()
.addInt32(length)
.add(bodyBuffer)
.flush()
}
return new Writer().addInt32(length).add(bodyBuffer).flush();
};
const requestSsl = (): Buffer => {
const response = Buffer.allocUnsafe(8)
const response = Buffer.allocUnsafe(8);
response.writeInt32BE(8, 0);
response.writeInt32BE(80877103, 4)
return response
}
response.writeInt32BE(80877103, 4);
return response;
};
const password = (password: string): Buffer => {
return writer.addCString(password).flush(code.startup)
}
return writer.addCString(password).flush(code.startup);
};
const sendSASLInitialResponseMessage = function (mechanism: string, initialResponse: string): Buffer {
// 0x70 = 'p'
writer
.addCString(mechanism)
.addInt32(Buffer.byteLength(initialResponse))
.addString(initialResponse)
writer.addCString(mechanism).addInt32(Buffer.byteLength(initialResponse)).addString(initialResponse);
return writer.flush(code.startup)
}
return writer.flush(code.startup);
};
const sendSCRAMClientFinalMessage = function (additionalData: string): Buffer {
return writer.addString(additionalData).flush(code.startup)
}
return writer.addString(additionalData).flush(code.startup);
};
const query = (text: string): Buffer => {
return writer.addCString(text).flush(code.query)
}
return writer.addCString(text).flush(code.query);
};
type ParseOpts = {
name?: string;
types?: number[];
text: string;
}
};
const emptyArray: any[] = []
const emptyArray: any[] = [];
const parse = (query: ParseOpts): Buffer => {
// expect something like this:
@ -82,171 +76,169 @@ const parse = (query: ParseOpts): Buffer => {
// types: ['int8', 'bool'] }
// normalize missing query names to allow for null
const name = query.name || ''
const name = query.name || '';
if (name.length > 63) {
/* eslint-disable no-console */
console.error('Warning! Postgres only supports 63 characters for query names.')
console.error('You supplied %s (%s)', name, name.length)
console.error('This can cause conflicts and silent errors executing queries')
console.error('Warning! Postgres only supports 63 characters for query names.');
console.error('You supplied %s (%s)', name, name.length);
console.error('This can cause conflicts and silent errors executing queries');
/* eslint-enable no-console */
}
const types = query.types || emptyArray
const types = query.types || emptyArray;
var len = types.length
var len = types.length;
var buffer = writer
.addCString(name) // name of query
.addCString(query.text) // actual query text
.addInt16(len)
.addInt16(len);
for (var i = 0; i < len; i++) {
buffer.addInt32(types[i])
buffer.addInt32(types[i]);
}
return writer.flush(code.parse)
}
return writer.flush(code.parse);
};
type BindOpts = {
portal?: string;
binary?: boolean;
statement?: string;
values?: any[];
}
};
const bind = (config: BindOpts = {}): Buffer => {
// normalize config
const portal = config.portal || ''
const statement = config.statement || ''
const binary = config.binary || false
var values = config.values || emptyArray
var len = values.length
const portal = config.portal || '';
const statement = config.statement || '';
const binary = config.binary || false;
var values = config.values || emptyArray;
var len = values.length;
var useBinary = false
var useBinary = false;
// TODO(bmc): all the loops in here aren't nice, we can do better
for (var j = 0; j < len; j++) {
useBinary = useBinary || values[j] instanceof Buffer
useBinary = useBinary || values[j] instanceof Buffer;
}
var buffer = writer
.addCString(portal)
.addCString(statement)
var buffer = writer.addCString(portal).addCString(statement);
if (!useBinary) {
buffer.addInt16(0)
buffer.addInt16(0);
} else {
buffer.addInt16(len)
buffer.addInt16(len);
for (j = 0; j < len; j++) {
buffer.addInt16(values[j] instanceof Buffer ? 1 : 0)
buffer.addInt16(values[j] instanceof Buffer ? 1 : 0);
}
}
buffer.addInt16(len)
buffer.addInt16(len);
for (var i = 0; i < len; i++) {
var val = values[i]
var val = values[i];
if (val === null || typeof val === 'undefined') {
buffer.addInt32(-1)
buffer.addInt32(-1);
} else if (val instanceof Buffer) {
buffer.addInt32(val.length)
buffer.add(val)
buffer.addInt32(val.length);
buffer.add(val);
} else {
buffer.addInt32(Buffer.byteLength(val))
buffer.addString(val)
buffer.addInt32(Buffer.byteLength(val));
buffer.addString(val);
}
}
if (binary) {
buffer.addInt16(1) // format codes to use binary
buffer.addInt16(1)
buffer.addInt16(1); // format codes to use binary
buffer.addInt16(1);
} else {
buffer.addInt16(0) // format codes to use text
buffer.addInt16(0); // format codes to use text
}
return writer.flush(code.bind)
}
return writer.flush(code.bind);
};
type ExecOpts = {
portal?: string;
rows?: number;
}
};
const emptyExecute = Buffer.from([code.execute, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00])
const emptyExecute = Buffer.from([code.execute, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00]);
const execute = (config?: ExecOpts): Buffer => {
// this is the happy path for most queries
if (!config || !config.portal && !config.rows) {
if (!config || (!config.portal && !config.rows)) {
return emptyExecute;
}
const portal = config.portal || ''
const rows = config.rows || 0
const portal = config.portal || '';
const rows = config.rows || 0;
const portalLength = Buffer.byteLength(portal)
const len = 4 + portalLength + 1 + 4
const portalLength = Buffer.byteLength(portal);
const len = 4 + portalLength + 1 + 4;
// one extra bit for code
const buff = Buffer.allocUnsafe(1 + len)
buff[0] = code.execute
buff.writeInt32BE(len, 1)
buff.write(portal, 5, 'utf-8')
const buff = Buffer.allocUnsafe(1 + len);
buff[0] = code.execute;
buff.writeInt32BE(len, 1);
buff.write(portal, 5, 'utf-8');
buff[portalLength + 5] = 0; // null terminate portal cString
buff.writeUInt32BE(rows, buff.length - 4)
buff.writeUInt32BE(rows, buff.length - 4);
return buff;
}
};
const cancel = (processID: number, secretKey: number): Buffer => {
const buffer = Buffer.allocUnsafe(16)
buffer.writeInt32BE(16, 0)
buffer.writeInt16BE(1234, 4)
buffer.writeInt16BE(5678, 6)
buffer.writeInt32BE(processID, 8)
buffer.writeInt32BE(secretKey, 12)
const buffer = Buffer.allocUnsafe(16);
buffer.writeInt32BE(16, 0);
buffer.writeInt16BE(1234, 4);
buffer.writeInt16BE(5678, 6);
buffer.writeInt32BE(processID, 8);
buffer.writeInt32BE(secretKey, 12);
return buffer;
}
};
type PortalOpts = {
type: 'S' | 'P',
type: 'S' | 'P';
name?: string;
}
};
const cstringMessage = (code: code, string: string): Buffer => {
const stringLen = Buffer.byteLength(string)
const len = 4 + stringLen + 1
const stringLen = Buffer.byteLength(string);
const len = 4 + stringLen + 1;
// one extra bit for code
const buffer = Buffer.allocUnsafe(1 + len)
buffer[0] = code
buffer.writeInt32BE(len, 1)
buffer.write(string, 5, 'utf-8')
buffer[len] = 0 // null terminate cString
return buffer
}
const buffer = Buffer.allocUnsafe(1 + len);
buffer[0] = code;
buffer.writeInt32BE(len, 1);
buffer.write(string, 5, 'utf-8');
buffer[len] = 0; // null terminate cString
return buffer;
};
const emptyDescribePortal = writer.addCString('P').flush(code.describe)
const emptyDescribeStatement = writer.addCString('S').flush(code.describe)
const emptyDescribePortal = writer.addCString('P').flush(code.describe);
const emptyDescribeStatement = writer.addCString('S').flush(code.describe);
const describe = (msg: PortalOpts): Buffer => {
return msg.name ?
cstringMessage(code.describe,`${msg.type}${msg.name || ''}`) :
msg.type === 'P' ?
emptyDescribePortal :
emptyDescribeStatement;
}
return msg.name
? cstringMessage(code.describe, `${msg.type}${msg.name || ''}`)
: msg.type === 'P'
? emptyDescribePortal
: emptyDescribeStatement;
};
const close = (msg: PortalOpts): Buffer => {
const text = `${msg.type}${msg.name || ''}`
return cstringMessage(code.close, text)
}
const text = `${msg.type}${msg.name || ''}`;
return cstringMessage(code.close, text);
};
const copyData = (chunk: Buffer): Buffer => {
return writer.add(chunk).flush(code.copyFromChunk)
}
return writer.add(chunk).flush(code.copyFromChunk);
};
const copyFail = (message: string): Buffer => {
return cstringMessage(code.copyFail, message);
}
};
const codeOnlyBuffer = (code: code): Buffer => Buffer.from([code, 0x00, 0x00, 0x00, 0x04])
const codeOnlyBuffer = (code: code): Buffer => Buffer.from([code, 0x00, 0x00, 0x00, 0x04]);
const flushBuffer = codeOnlyBuffer(code.flush)
const syncBuffer = codeOnlyBuffer(code.sync)
const endBuffer = codeOnlyBuffer(code.end)
const copyDoneBuffer = codeOnlyBuffer(code.copyDone)
const flushBuffer = codeOnlyBuffer(code.flush);
const syncBuffer = codeOnlyBuffer(code.sync);
const endBuffer = codeOnlyBuffer(code.end);
const copyDoneBuffer = codeOnlyBuffer(code.copyDone);
const serialize = {
startup,
@ -266,7 +258,7 @@ const serialize = {
copyData,
copyDone: () => copyDoneBuffer,
copyFail,
cancel
}
cancel,
};
export { serialize }
export { serialize };

View File

@ -1,79 +1,75 @@
export default class BufferList {
constructor(public buffers: Buffer[] = []) {
}
constructor(public buffers: Buffer[] = []) {}
public add(buffer: Buffer, front?: boolean) {
this.buffers[front ? 'unshift' : 'push'](buffer)
return this
this.buffers[front ? 'unshift' : 'push'](buffer);
return this;
}
public addInt16(val: number, front?: boolean) {
return this.add(Buffer.from([(val >>> 8), (val >>> 0)]), front)
return this.add(Buffer.from([val >>> 8, val >>> 0]), front);
}
public getByteLength(initial?: number) {
return this.buffers.reduce(function (previous, current) {
return previous + current.length
}, initial || 0)
return previous + current.length;
}, initial || 0);
}
public addInt32(val: number, first?: boolean) {
return this.add(Buffer.from([
(val >>> 24 & 0xFF),
(val >>> 16 & 0xFF),
(val >>> 8 & 0xFF),
(val >>> 0 & 0xFF)
]), first)
return this.add(
Buffer.from([(val >>> 24) & 0xff, (val >>> 16) & 0xff, (val >>> 8) & 0xff, (val >>> 0) & 0xff]),
first
);
}
public addCString(val: string, front?: boolean) {
var len = Buffer.byteLength(val)
var buffer = Buffer.alloc(len + 1)
buffer.write(val)
buffer[len] = 0
return this.add(buffer, front)
var len = Buffer.byteLength(val);
var buffer = Buffer.alloc(len + 1);
buffer.write(val);
buffer[len] = 0;
return this.add(buffer, front);
}
public addString(val: string, front?: boolean) {
var len = Buffer.byteLength(val)
var buffer = Buffer.alloc(len)
buffer.write(val)
return this.add(buffer, front)
var len = Buffer.byteLength(val);
var buffer = Buffer.alloc(len);
buffer.write(val);
return this.add(buffer, front);
}
public addChar(char: string, first?: boolean) {
return this.add(Buffer.from(char, 'utf8'), first)
return this.add(Buffer.from(char, 'utf8'), first);
}
public addByte(byte: number) {
return this.add(Buffer.from([byte]))
return this.add(Buffer.from([byte]));
}
public join(appendLength?: boolean, char?: string): Buffer {
var length = this.getByteLength()
var length = this.getByteLength();
if (appendLength) {
this.addInt32(length + 4, true)
return this.join(false, char)
this.addInt32(length + 4, true);
return this.join(false, char);
}
if (char) {
this.addChar(char, true)
length++
this.addChar(char, true);
length++;
}
var result = Buffer.alloc(length)
var index = 0
var result = Buffer.alloc(length);
var index = 0;
this.buffers.forEach(function (buffer) {
buffer.copy(result, index, 0)
index += buffer.length
})
return result
buffer.copy(result, index, 0);
index += buffer.length;
});
return result;
}
public static concat(): Buffer {
var total = new BufferList()
var total = new BufferList();
for (var i = 0; i < arguments.length; i++) {
total.add(arguments[i])
total.add(arguments[i]);
}
return total.join()
return total.join();
}
}

View File

@ -1,150 +1,123 @@
// http://developer.postgresql.org/pgdocs/postgres/protocol-message-formats.html
import BufferList from './buffer-list'
import BufferList from './buffer-list';
const buffers = {
readyForQuery: function () {
return new BufferList()
.add(Buffer.from('I'))
.join(true, 'Z')
return new BufferList().add(Buffer.from('I')).join(true, 'Z');
},
authenticationOk: function () {
return new BufferList()
.addInt32(0)
.join(true, 'R')
return new BufferList().addInt32(0).join(true, 'R');
},
authenticationCleartextPassword: function () {
return new BufferList()
.addInt32(3)
.join(true, 'R')
return new BufferList().addInt32(3).join(true, 'R');
},
authenticationMD5Password: function () {
return new BufferList()
.addInt32(5)
.add(Buffer.from([1, 2, 3, 4]))
.join(true, 'R')
.join(true, 'R');
},
authenticationSASL: function () {
return new BufferList()
.addInt32(10)
.addCString('SCRAM-SHA-256')
.addCString('')
.join(true, 'R')
return new BufferList().addInt32(10).addCString('SCRAM-SHA-256').addCString('').join(true, 'R');
},
authenticationSASLContinue: function () {
return new BufferList()
.addInt32(11)
.addString('data')
.join(true, 'R')
return new BufferList().addInt32(11).addString('data').join(true, 'R');
},
authenticationSASLFinal: function () {
return new BufferList()
.addInt32(12)
.addString('data')
.join(true, 'R')
return new BufferList().addInt32(12).addString('data').join(true, 'R');
},
parameterStatus: function (name: string, value: string) {
return new BufferList()
.addCString(name)
.addCString(value)
.join(true, 'S')
return new BufferList().addCString(name).addCString(value).join(true, 'S');
},
backendKeyData: function (processID: number, secretKey: number) {
return new BufferList()
.addInt32(processID)
.addInt32(secretKey)
.join(true, 'K')
return new BufferList().addInt32(processID).addInt32(secretKey).join(true, 'K');
},
commandComplete: function (string: string) {
return new BufferList()
.addCString(string)
.join(true, 'C')
return new BufferList().addCString(string).join(true, 'C');
},
rowDescription: function (fields: any[]) {
fields = fields || []
var buf = new BufferList()
buf.addInt16(fields.length)
fields = fields || [];
var buf = new BufferList();
buf.addInt16(fields.length);
fields.forEach(function (field) {
buf.addCString(field.name)
buf
.addCString(field.name)
.addInt32(field.tableID || 0)
.addInt16(field.attributeNumber || 0)
.addInt32(field.dataTypeID || 0)
.addInt16(field.dataTypeSize || 0)
.addInt32(field.typeModifier || 0)
.addInt16(field.formatCode || 0)
})
return buf.join(true, 'T')
.addInt16(field.formatCode || 0);
});
return buf.join(true, 'T');
},
dataRow: function (columns: any[]) {
columns = columns || []
var buf = new BufferList()
buf.addInt16(columns.length)
columns = columns || [];
var buf = new BufferList();
buf.addInt16(columns.length);
columns.forEach(function (col) {
if (col == null) {
buf.addInt32(-1)
buf.addInt32(-1);
} else {
var strBuf = Buffer.from(col, 'utf8')
buf.addInt32(strBuf.length)
buf.add(strBuf)
var strBuf = Buffer.from(col, 'utf8');
buf.addInt32(strBuf.length);
buf.add(strBuf);
}
})
return buf.join(true, 'D')
});
return buf.join(true, 'D');
},
error: function (fields: any) {
return buffers.errorOrNotice(fields).join(true, 'E')
return buffers.errorOrNotice(fields).join(true, 'E');
},
notice: function (fields: any) {
return buffers.errorOrNotice(fields).join(true, 'N')
return buffers.errorOrNotice(fields).join(true, 'N');
},
errorOrNotice: function (fields: any) {
fields = fields || []
var buf = new BufferList()
fields = fields || [];
var buf = new BufferList();
fields.forEach(function (field: any) {
buf.addChar(field.type)
buf.addCString(field.value)
})
return buf.add(Buffer.from([0]))// terminator
buf.addChar(field.type);
buf.addCString(field.value);
});
return buf.add(Buffer.from([0])); // terminator
},
parseComplete: function () {
return new BufferList().join(true, '1')
return new BufferList().join(true, '1');
},
bindComplete: function () {
return new BufferList().join(true, '2')
return new BufferList().join(true, '2');
},
notification: function (id: number, channel: string, payload: string) {
return new BufferList()
.addInt32(id)
.addCString(channel)
.addCString(payload)
.join(true, 'A')
return new BufferList().addInt32(id).addCString(channel).addCString(payload).join(true, 'A');
},
emptyQuery: function () {
return new BufferList().join(true, 'I')
return new BufferList().join(true, 'I');
},
portalSuspended: function () {
return new BufferList().join(true, 's')
return new BufferList().join(true, 's');
},
closeComplete: function () {
return new BufferList().join(true, '3')
return new BufferList().join(true, '3');
},
copyIn: function (cols: number) {
@ -156,7 +129,7 @@ const buffers = {
for (let i = 0; i < cols; i++) {
list.addInt16(i);
}
return list.join(true, 'G')
return list.join(true, 'G');
},
copyOut: function (cols: number) {
@ -168,7 +141,7 @@ const buffers = {
for (let i = 0; i < cols; i++) {
list.addInt16(i);
}
return list.join(true, 'H')
return list.join(true, 'H');
},
copyData: function (bytes: Buffer) {
@ -176,8 +149,8 @@ const buffers = {
},
copyDone: function () {
return new BufferList().join(true, 'c')
}
}
return new BufferList().join(true, 'c');
},
};
export default buffers
export default buffers;

View File

@ -1 +1 @@
declare module 'chunky'
declare module 'chunky';

View File

@ -7,7 +7,7 @@ params := $(connectionString)
node-command := xargs -n 1 -I file node file $(params)
.PHONY : test test-connection test-integration bench test-native \
lint publish test-missing-native update-npm
publish test-missing-native update-npm
all:
npm install
@ -17,7 +17,7 @@ help:
test: test-unit
test-all: lint test-missing-native test-unit test-integration test-native
test-all: test-missing-native test-unit test-integration test-native
update-npm:
@ -59,7 +59,3 @@ test-binary: test-connection
test-pool:
@find test/integration/connection-pool -name "*.js" | $(node-command) binary
lint:
@echo "***Starting lint***"
node_modules/.bin/eslint lib

View File

@ -836,6 +836,11 @@
resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.2.7.tgz#1c8c25cbf6e59ffa7d6b9652c78e547d9a41692d"
integrity sha512-luq8meHGYwvky0O7u0eQZdA7B4Wd9owUCqvbw2m3XCrCU8mplYOujMBbvyS547AxJkC+pGnd0Cm15eNxEUNU8g==
"@types/eslint-visitor-keys@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d"
integrity sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==
"@types/events@*":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7"
@ -890,6 +895,16 @@
eslint-scope "^5.0.0"
eslint-utils "^2.0.0"
"@typescript-eslint/parser@^2.27.0":
version "2.27.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-2.27.0.tgz#d91664335b2c46584294e42eb4ff35838c427287"
integrity sha512-HFUXZY+EdwrJXZo31DW4IS1ujQW3krzlRjBrFRrJcMDh0zCu107/nRfhk/uBasO8m0NVDbBF5WZKcIUMRO7vPg==
dependencies:
"@types/eslint-visitor-keys" "^1.0.0"
"@typescript-eslint/experimental-utils" "2.27.0"
"@typescript-eslint/typescript-estree" "2.27.0"
eslint-visitor-keys "^1.1.0"
"@typescript-eslint/typescript-estree@2.27.0":
version "2.27.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.27.0.tgz#a288e54605412da8b81f1660b56c8b2e42966ce8"