diff --git a/README.md b/README.md index 1738026..a772997 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,7 @@ const handler = async(event, context) => { | maxRetries | `Integer` | Maximum number of times to retry a connection before throwing an error. | `3` | | processCountCacheEnabled | `Boolean` | Enable caching for get process count. | `False` | | processCountFreqMs | `Integer` | The number of milliseconds to cache lookups of process count. | `6000` | +| allowCredentialsDiffing | `Boolean` | If you are using dynamic credentials, such as IAM, you can set this parameter to `true` and the client will be refreshed | `false` | ## Note diff --git a/__tests__/Dockerfile b/__tests__/Dockerfile deleted file mode 100644 index eaa7cce..0000000 --- a/__tests__/Dockerfile +++ /dev/null @@ -1,5 +0,0 @@ -FROM postgres - -RUN apt-get update - -CMD ["postgres", "-c", "log_statement=all", "-c", "log_destination=stderr"] diff --git a/__tests__/index.test.js b/__tests__/index.test.js index e1a7be4..6f8cd3c 100644 --- a/__tests__/index.test.js +++ b/__tests__/index.test.js @@ -2,6 +2,14 @@ const ServerlessClient = require("../index"); jest.setTimeout(30000); +const dbConfig = { + user: "postgres", + host: "localhost", + database: "postgres", + password: "postgres", + port: 22000 +} + const sleep = delay => new Promise(resolve => { setTimeout(() => { @@ -17,7 +25,7 @@ const generateMockConnections = async (n) => { host: "localhost", database: "postgres", password: "postgres", - port: 5433 + port: dbConfig.port }); await c.connect(); @@ -41,27 +49,27 @@ const cleanMockConnections = async (clients) => { * they will not be run in the CI/CD pipeline but only on pre-commit to make sure nothing is broken. * Make sure your PostgreSQL is running and no other clients other than the test user is connected. */ -describe("Serverless client", function() { - describe("Default strategy minimum_idle_time", function() { +describe("Serverless client", function () { + describe("Default strategy minimum_idle_time", function () { let client; - beforeEach(async function() { + beforeEach(async function () { client = new ServerlessClient({ user: "postgres", host: "localhost", database: "postgres", password: "postgres", - port: 5433, + port: dbConfig.port, connUtilization: 0.09 }); await client.connect(); }); - afterEach(async function() { + afterEach(async function () { await client.end(); }); - it("should get the list of idles connections", async function() { + it("should get the list of idles connections", async function () { const mockClients = await generateMockConnections(10); await sleep(1000); await mockClients[0].query("SELECT 1+1 AS result"); // make a random client not idle @@ -74,7 +82,7 @@ describe("Serverless client", function() { expect(result).toHaveLength(9); }); - it("should get the list of idles connections with limit", async function() { + it("should get the list of idles connections with limit", async function () { client._strategy.maxIdleConnectionsToKill = 5; const mockClients = await generateMockConnections(10); await sleep(1000); @@ -85,7 +93,7 @@ describe("Serverless client", function() { expect(result).toHaveLength(5); }); - it("should get an empty list of idles connections, idle time not passed", async function() { + it("should get an empty list of idles connections, idle time not passed", async function () { client._strategy.minConnIdleTimeSec = 2; const mockClients = await generateMockConnections(10); await sleep(1000); @@ -96,7 +104,7 @@ describe("Serverless client", function() { expect(result).toHaveLength(0); }); - it("should kill the right number of connections", async function() { + it("should kill the right number of connections", async function () { const mockClients = await generateMockConnections(10); await sleep(1000); await mockClients[0].query("SELECT 1+1 AS result"); // make a random client not idle @@ -107,13 +115,13 @@ describe("Serverless client", function() { expect(result).toHaveLength(9); }); - it("should set max connections from the db", async function() { + it("should set max connections from the db", async function () { const client1 = new ServerlessClient({ user: "postgres", host: "localhost", database: "postgres", password: "postgres", - port: 5433, + port: dbConfig.port, connUtilization: 0.09, debug: false, manualMaxConnections: true, @@ -128,13 +136,13 @@ describe("Serverless client", function() { await client1.end(); }); - it("should cache process count", async function() { + it("should cache process count", async function () { const client1 = new ServerlessClient({ user: "postgres", host: "localhost", database: "postgres", password: "postgres", - port: 5433, + port: dbConfig.port, processCountCacheEnabled: true, processCountFreqMs: 2000, debug: true @@ -170,16 +178,16 @@ describe("Serverless client", function() { }); }); - describe("Ranked strategy", function() { + describe("Ranked strategy", function () { let client; - beforeEach(async function() { + beforeEach(async function () { client = new ServerlessClient({ user: "postgres", host: "localhost", database: "postgres", password: "postgres", - port: 5433, + port: dbConfig.port, connUtilization: 0.09, debug: false, strategy: "ranked" @@ -187,11 +195,11 @@ describe("Serverless client", function() { await client.connect(); }); - afterEach(async function() { + afterEach(async function () { await client.end(); }); - it("should get process count without errors", async function() { + it("should get process count without errors", async function () { const mockClients = await generateMockConnections(4); const result = await client._getProcessesCount(); await cleanMockConnections(mockClients); @@ -199,7 +207,7 @@ describe("Serverless client", function() { expect(result).toBe("5"); }); - it("should get list of processes count", async function() { + it("should get list of processes count", async function () { const mockClients = await generateMockConnections(4); const result = await client._getIdleProcessesListOrderByDate(); await cleanMockConnections(mockClients); @@ -208,14 +216,14 @@ describe("Serverless client", function() { expect(result.map(r => r.state)).toEqual(["idle", "idle", "idle", "idle"]); }); - it("should clean idle clients", async function() { + it("should clean idle clients", async function () { await generateMockConnections(10); const result = await client.clean(); expect(result).toHaveLength(10); }); - it("should not clean idle clients not enough clients connected", async function() { + it("should not clean idle clients not enough clients connected", async function () { const mockClients = await generateMockConnections(4); const result = await client.clean(); await cleanMockConnections(mockClients); @@ -224,15 +232,15 @@ describe("Serverless client", function() { }); }); - describe("Client", function() { - it("should try to reconnect and fail after 3 attempt", async function() { + describe("Client", function () { + it("should try to reconnect and fail after 3 attempt", async function () { const mockClients = await generateMockConnections(100); const client = new ServerlessClient({ user: "postgres", host: "localhost", database: "postgres", password: "postgres", - port: 5433, + port: dbConfig.port, debug: true }); @@ -248,7 +256,7 @@ describe("Serverless client", function() { } }); - it("should try to reconnect and succeed", async function() { + it("should try to reconnect and succeed", async function () { const mockClients = await generateMockConnections(100); // End this client after one second so the connection reattempt can be successful @@ -261,7 +269,7 @@ describe("Serverless client", function() { host: "localhost", database: "postgres", password: "postgres", - port: 5433, + port: dbConfig.port, debug: true }); @@ -277,13 +285,13 @@ describe("Serverless client", function() { }); - it("Should reinitialize a client and query again", async function() { + it("Should reinitialize a client and query again", async function () { const client = new ServerlessClient({ user: "postgres", host: "localhost", database: "postgres", password: "postgres", - port: 5433, + port: dbConfig.port, connUtilization: 0.09 }); await client.connect(); @@ -291,7 +299,7 @@ describe("Serverless client", function() { const mockClients = await generateMockConnections(1); const pid = mockClients[0]._client.processID; // Simulate the process being killed by serverless-postgres - await client._killProcesses([{ pid }]); + await client._killProcesses([{pid}]); // Try to query to a client that has been disconnected const result = await mockClients[0].query("SELECT 1+1 AS result"); @@ -304,12 +312,12 @@ describe("Serverless client", function() { expect(result.rows[0].result).toBe(2); }); - it("should set password in the config and connect without errors", async function() { + it("should set password in the config and connect without errors", async function () { const client = new ServerlessClient({ user: "postgres", host: "localhost", database: "postgres", - port: 5433, + port: dbConfig.port, connUtilization: 0.09 }); client.setConfig({ @@ -321,9 +329,63 @@ describe("Serverless client", function() { expect(result.rows[0].result).toBe(2); }); + + it('should create a new client if new credentials are passed', async function () { + const diffCredentialsSpy = jest.spyOn(ServerlessClient.prototype, "_diffCredentials") + const client = new ServerlessClient({ + allowCredentialsDiffing: true + }); + + expect(diffCredentialsSpy).not.toHaveBeenCalled() + + client.setConfig({ + user: "postgres", + host: "localhost", + database: "postgres", + password: "postgres", + port: dbConfig.port, + }) + await client.connect(); + await client.clean(); + + expect(diffCredentialsSpy).not.toHaveBeenCalled() + + // Switch new credentials + client.setConfig({ + user: "postgres2", + host: "localhost", + database: "postgres2", + password: "postgres2", + port: 22001, + }) + await client.connect(); + + expect(diffCredentialsSpy).toHaveBeenCalledTimes(1) + expect(client._config).toEqual({ + allowCredentialsDiffing: true, + user: "postgres2", + host: "localhost", + database: "postgres2", + password: "postgres2", + port: 22001, + }) + + // Switch again database to the previous one + client.setConfig({ + user: "postgres", + host: "localhost", + database: "postgres", + password: "postgres", + port: dbConfig.port, + }) + await client.connect(); + await client.end(); + + expect(diffCredentialsSpy).toHaveBeenCalledTimes(2) + }); }); - describe("Validation", function() { + describe("Validation", function () { [ { name: "should reject the instantiation an invalid strategy", @@ -333,7 +395,7 @@ describe("Serverless client", function() { host: "localhost", database: "postgres", password: "postgres", - port: 5433, + port: dbConfig.port, strategy: "invalid_strategy" } }, @@ -349,7 +411,7 @@ describe("Serverless client", function() { host: "localhost", database: "postgres", password: "postgres", - port: 5433, + port: dbConfig.port, debug: [] } }, @@ -365,7 +427,7 @@ describe("Serverless client", function() { host: "localhost", database: "postgres", password: "postgres", - port: 5433, + port: dbConfig.port, maxIdleConnectionsToKill: "null" } }, @@ -381,7 +443,7 @@ describe("Serverless client", function() { host: "localhost", database: "postgres", password: "postgres", - port: 5433, + port: dbConfig.port, maxConnsFreqMs: -200 } }, @@ -397,7 +459,7 @@ describe("Serverless client", function() { host: "localhost", database: "postgres", password: "postgres", - port: 5433, + port: dbConfig.port, connUtilization: 1.2 } }, @@ -406,7 +468,7 @@ describe("Serverless client", function() { } } ].forEach(test => { - it(test.name, function() { + it(test.name, function () { try { new ServerlessClient(test.args.config); @@ -418,12 +480,12 @@ describe("Serverless client", function() { }); }); - it("should persist the config object even when setConfig is called", async function() { + it("should persist the config object even when setConfig is called", async function () { const client = new ServerlessClient({ user: "postgres", host: "localhost", database: "postgres", - port: 5433, + port: dbConfig.port, connUtilization: 0.09, debug: true, maxConnections: 500 @@ -439,13 +501,13 @@ describe("Serverless client", function() { expect(client._maxConns.cache.total).toBe(500); }); - it("should setConfig correctly and override previous options", async function() { + it("should setConfig correctly and override previous options", async function () { const client = new ServerlessClient({ user: "wrong user", host: "localhost", database: "postgres", password: "wrong password", - port: 5433, + port: dbConfig.port, connUtilization: 0.09, debug: true, maxConnections: 500 @@ -464,35 +526,35 @@ describe("Serverless client", function() { expect(client._maxConns.cache.total).toBe(8); }); - it("should be able to connect with connection string", async function() { + it("should be able to connect with connection string", async function () { const client = new ServerlessClient({ - connectionString: "postgresql://postgres:postgres@localhost:5433/postgres", + connectionString: `postgresql://postgres:postgres@localhost:${dbConfig.port}/postgres`, debug: true }); await client.connect(); expect(client._client.database).toEqual("postgres"); - expect(client._client.port).toEqual(5433); + expect(client._client.port).toEqual(dbConfig.port); expect(client._client.user).toEqual("postgres"); await client.end(); }); - it("should be able to connect with connection string through setConfig", async function() { + it("should be able to connect with connection string through setConfig", async function () { const client = new ServerlessClient({ debug: true }); client.setConfig({ - connectionString: "postgresql://postgres:postgres@localhost:5433/postgres", + connectionString: `postgresql://postgres:postgres@localhost:${dbConfig.port}/postgres`, maxConnections: 10 }); await client.connect(); expect(client._client.database).toEqual("postgres"); - expect(client._client.port).toEqual(5433); + expect(client._client.port).toEqual(dbConfig.port); expect(client._client.user).toEqual("postgres"); await client.end(); diff --git a/__tests__/start-db.sh b/__tests__/start-db.sh deleted file mode 100644 index a412b3e..0000000 --- a/__tests__/start-db.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env bash - -docker build -t postgres . && -docker run \ - --rm \ - -p 5432:5432 \ - --env POSTGRES_PASSWORD=postgres \ - --name postgres \ - postgres diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..fc4b60e --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,17 @@ +version: "3.9" +services: + postgres: + image: postgres:10-alpine + environment: + - POSTGRES_PASSWORD=postgres + - POSTGRES_USER=postgres + ports: + - "22000:5432" + postgres2: + image: postgres:10-alpine + environment: + - POSTGRES_PASSWORD=postgres2 + - POSTGRES_USER=postgres2 + - POSTGRES_DATABSE=postgres2 + ports: + - "22001:5432" \ No newline at end of file diff --git a/index.d.ts b/index.d.ts index 9aa4640..2d51092 100644 --- a/index.d.ts +++ b/index.d.ts @@ -14,6 +14,7 @@ declare interface Config { port?: number; host?: string; connectionString?: string; + allowCredentialsDiffing?: boolean; keepAlive?: boolean; stream?: stream.Duplex; statement_timeout?: false | number; diff --git a/package.json b/package.json index d03c89e..6595a35 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "", "main": "index.js", "scripts": { - "test": "jest", + "test": "docker-compose up --build -d && jest && docker-compose down", "semantic-release": "semantic-release" }, "author": "", diff --git a/src/index.js b/src/index.js index 973c313..3f11f6f 100644 --- a/src/index.js +++ b/src/index.js @@ -7,8 +7,8 @@ * @license MIT */ -const { isValidStrategy, type, validateNum, isWithinRange } = require("./utils"); -const { Client } = require("pg"); +const {isValidStrategy, type, validateNum, isWithinRange} = require("./utils"); +const {Client} = require("pg"); function ServerlessClient(config) { this._client = null; @@ -40,15 +40,15 @@ ServerlessClient.prototype._setMaxConnections = async (__self) => { // This strategy arbitrarily (maxIdleConnections) terminates connections starting from the oldest one in idle. // It is very aggressive and it can cause disruption if a connection was in idle for a short period of time -ServerlessClient.prototype._getIdleProcessesListOrderByDate = async function() { +ServerlessClient.prototype._getIdleProcessesListOrderByDate = async function () { const query = ` - SELECT pid,backend_start,state - FROM pg_stat_activity - WHERE datname=$1 - AND state='idle' - AND usename=$2 - ORDER BY state_change - LIMIT $3;` + SELECT pid, backend_start, state + FROM pg_stat_activity + WHERE datname = $1 + AND state = 'idle' + AND usename = $2 + ORDER BY state_change + LIMIT $3;` const values = [ this._client.database, @@ -60,7 +60,7 @@ ServerlessClient.prototype._getIdleProcessesListOrderByDate = async function() { const result = await this._client.query(query, values); return result.rows - } catch (e){ + } catch (e) { this._logger("Swallowed internal error", e.message) // Swallow the error, if this produce an error there is no need to error the function return [] @@ -70,21 +70,20 @@ ServerlessClient.prototype._getIdleProcessesListOrderByDate = async function() { // This strategy select only the connections that have been in idle state for more // than a minimum amount of seconds, it is very accurate as it only takes the process that have been in idle // for more than a threshold time (minConnectionTimeoutSec) -ServerlessClient.prototype._getIdleProcessesListByMinimumTimeout = async function(){ +ServerlessClient.prototype._getIdleProcessesListByMinimumTimeout = async function () { const query = ` - WITH processes AS( - SELECT - EXTRACT(EPOCH FROM (Now() - state_change)) AS idle_time, - pid - FROM pg_stat_activity - WHERE usename=$1 - AND datname=$2 - AND state='idle' - ) - SELECT pid - FROM processes - WHERE idle_time > $3 - LIMIT $4;` + WITH processes AS ( + SELECT EXTRACT(EPOCH FROM (Now() - state_change)) AS idle_time, + pid + FROM pg_stat_activity + WHERE usename = $1 + AND datname = $2 + AND state = 'idle' + ) + SELECT pid + FROM processes + WHERE idle_time > $3 + LIMIT $4;` const values = [ this._client.user, @@ -104,7 +103,7 @@ ServerlessClient.prototype._getIdleProcessesListByMinimumTimeout = async functio } } -ServerlessClient.prototype._getProcessesCount = async function() { +ServerlessClient.prototype._getProcessesCount = async function () { function isCacheExpiredOrDisabled(__self) { // If cache is disabled if (!__self._processCount.cacheEnabled) { @@ -142,19 +141,20 @@ ServerlessClient.prototype._getProcessesCount = async function() { return this._processCount.cache.count }; -ServerlessClient.prototype._killProcesses = async function(processesList) { +ServerlessClient.prototype._killProcesses = async function (processesList) { const pids = processesList.map(proc => proc.pid); const query = ` - SELECT pg_terminate_backend(pid) - FROM pg_stat_activity - WHERE pid = ANY ($1) AND state='idle';` + SELECT pg_terminate_backend(pid) + FROM pg_stat_activity + WHERE pid = ANY ($1) + AND state = 'idle';` const values = [pids] try { return await this._client.query(query, values) - } catch (e){ + } catch (e) { this._logger("Swallowed internal error", e.message) // Swallow the error, if this produce an error there is no need to error the function @@ -164,7 +164,7 @@ ServerlessClient.prototype._killProcesses = async function(processesList) { } }; -ServerlessClient.prototype._getStrategy = function(){ +ServerlessClient.prototype._getStrategy = function () { switch (this._strategy.name) { case "minimum_idle_time": return this._getIdleProcessesListByMinimumTimeout.bind(this) @@ -175,14 +175,14 @@ ServerlessClient.prototype._getStrategy = function(){ } } -ServerlessClient.prototype._decorrelatedJitter = function(delay){ +ServerlessClient.prototype._decorrelatedJitter = function (delay) { const cap = this._backoff.capMs; const base = this._backoff.baseMs; - const randRange = (min,max) => Math.floor(Math.random() * (max - min + 1)) + min; + const randRange = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min; return Math.min(cap, randRange(base, delay * 3)); } -ServerlessClient.prototype.clean = async function() { +ServerlessClient.prototype.clean = async function () { const processCount = await this._getProcessesCount(); this._logger("Current process count: ", processCount); @@ -197,12 +197,29 @@ ServerlessClient.prototype.clean = async function() { } }; -ServerlessClient.prototype._init = async function(){ - if(this._client !== null){ +ServerlessClient.prototype._diffCredentials = function (config) { + const keys = ['password', 'host', 'port', 'user', 'database'] + for (const key of keys) { + if (this._config[key] !== config[key]) { + this._multipleCredentials.areCredentialsDifferent = true + break; + } + } +} + +ServerlessClient.prototype._init = async function () { + if (this._client !== null && !this._multipleCredentials.areCredentialsDifferent) { return } + if (this._client !== null && this._multipleCredentials.areCredentialsDifferent) { + // For the time being we close the connection if new credentials are detected to avoid leaking. + // In the future we could use Pool in this case to avoid recreating a client each time + this._client.end() + } + this._client = new Client(this._config) + this._multipleCredentials.areCredentialsDifferent = false // pg throws an error if we terminate the connection, therefore we need to swallow these errors // and throw the rest @@ -223,14 +240,14 @@ ServerlessClient.prototype._init = async function(){ await this._client.connect(); this._logger("Connected...") - if (this._maxConns.manualMaxConnections){ + if (this._maxConns.manualMaxConnections) { await this._setMaxConnections(this) } this._logger("Max connections: ", this._maxConns.cache.total) } -ServerlessClient.prototype._validateConfig = function(config){ +ServerlessClient.prototype._validateConfig = function (config) { const { manualMaxConnections, maxConnsFreqMs, @@ -249,58 +266,64 @@ ServerlessClient.prototype._validateConfig = function(config){ if ( manualMaxConnections && type(manualMaxConnections) !== "Boolean" - ){ + ) { throw new Error("manualMaxConnections must be of type Boolean") } - if (debug && type(debug) !== "Boolean"){ + if (debug && type(debug) !== "Boolean") { throw new Error("debug must be of type Boolean") } - if (validateNum(maxConnsFreqMs)){ + if (validateNum(maxConnsFreqMs)) { throw new Error("maxConnsFreqMs must be of type Number") } - if (validateNum(maxConnections)){ + if (validateNum(maxConnections)) { throw new Error("maxConnections must be of type Number") } - if (strategy && !isValidStrategy(strategy)){ + if (strategy && !isValidStrategy(strategy)) { throw new Error("the provided strategy is invalid") } - if (validateNum(maxIdleConnectionsToKill)){ + if (validateNum(maxIdleConnectionsToKill)) { throw new Error("maxIdleConnectionsToKill must be of type Number or null") } - if (validateNum(minConnectionIdleTimeSec)){ + if (validateNum(minConnectionIdleTimeSec)) { throw new Error("minConnectionIdleTimeSec must be of type Number") } - if (validateNum(connUtilization) || !isWithinRange(connUtilization, 0, 1)){ + if (validateNum(connUtilization) || !isWithinRange(connUtilization, 0, 1)) { throw new Error("connUtilization must be of type Number") } - if (validateNum(capMs)){ + if (validateNum(capMs)) { throw new Error("capMs must be of type Number") } - if (validateNum(baseMs)){ + if (validateNum(baseMs)) { throw new Error("baseMs must be of type Number") } - if (validateNum(delayMs)){ + if (validateNum(delayMs)) { throw new Error("delayMs must be of type Number") } - if (validateNum(maxRetries)){ + if (validateNum(maxRetries)) { throw new Error("maxRetries must be of type Number") } } ServerlessClient.prototype.setConfig = function (config) { + const prevConfig = this._config; this._validateConfig(config) - this._config = { ...this._config, ...config }; + this._config = {...this._config, ...config}; + + this._multipleCredentials = { + allowCredentialsDiffing: this._config.allowCredentialsDiffing || false, + areCredentialsDifferent: false + }; this._maxConns = { // Cache expiration for getting the max connections value in milliseconds @@ -353,16 +376,21 @@ ServerlessClient.prototype.setConfig = function (config) { retries: 0, queryRetries: 0 } + + // Prevent diffing also if client is null + if (this._multipleCredentials.allowCredentialsDiffing && this._client !== null) { + this._diffCredentials(prevConfig, config) + } } -ServerlessClient.prototype._logger = function(...args) { - if (this._debug){ +ServerlessClient.prototype._logger = function (...args) { + if (this._debug) { const pid = this._client && this._client.processID || 'offline' - console.log('serverless-pg | pid: ', pid, ' | ', ...args) + console.log('serverless-pg | pid: ', pid, ' | ', ...args) } } -ServerlessClient.prototype.connect = async function() { +ServerlessClient.prototype.connect = async function () { try { await this._init(); } catch (e) { @@ -391,7 +419,7 @@ ServerlessClient.prototype.connect = async function() { } }; -ServerlessClient.prototype.query = async function(...args){ +ServerlessClient.prototype.query = async function (...args) { try { this._logger("Start query...") // We fulfill the promise to catch the error @@ -401,7 +429,7 @@ ServerlessClient.prototype.query = async function(...args){ e.message === "Client has encountered a connection error and is not queryable" || e.message === "terminating connection due to administrator command" || e.message === "Connection terminated unexpectedly" - ){ + ) { // If a client has been terminated by serverless-postgres and try to query again // we re-initialize it and retry this._client = null @@ -424,15 +452,15 @@ ServerlessClient.prototype.query = async function(...args){ } } -ServerlessClient.prototype.end = async function(){ +ServerlessClient.prototype.end = async function () { this._backoff.retries = 0 this._backoff.queryRetries = 0 await this._client.end() this._client = null } -ServerlessClient.prototype.on = function(...args){ +ServerlessClient.prototype.on = function (...args) { this._client.on(...args) } -module.exports = { ServerlessClient }; +module.exports = {ServerlessClient};