diff --git a/spec/PostgresConfigParser.spec.js b/spec/PostgresConfigParser.spec.js new file mode 100644 index 00000000..0cb84cf4 --- /dev/null +++ b/spec/PostgresConfigParser.spec.js @@ -0,0 +1,65 @@ +const parser = require('../src/Adapters/Storage/Postgres/PostgresConfigParser'); + +const queryParamTests = { + 'a=1&b=2': { a: '1', b: '2' }, + 'a=abcd%20efgh&b=abcd%3Defgh': { a: 'abcd efgh', b: 'abcd=efgh' }, + 'a=1&b&c=true': { a: '1', b: '', c: 'true' } +} + +describe('PostgresConfigParser.parseQueryParams', () => { + it('creates a map from a query string', () => { + + for (const key in queryParamTests) { + const result = parser.parseQueryParams(key); + + const testObj = queryParamTests[key]; + + expect(Object.keys(result).length) + .toEqual(Object.keys(testObj).length); + + for (const k in result) { + expect(result[k]).toEqual(testObj[k]); + } + } + + }) +}); + +const baseURI = 'postgres://username:password@localhost:5432/db-name' + +const dbOptionsTest = {}; +dbOptionsTest[`${baseURI}?ssl=true&binary=true&application_name=app_name&fallback_application_name=f_app_name&poolSize=10`] = { + ssl: true, + binary: true, + application_name: 'app_name', + fallback_application_name: 'f_app_name', + poolSize: 10 +}; +dbOptionsTest[`${baseURI}?ssl=&binary=aa`] = { + ssl: false, + binary: false +} + +describe('PostgresConfigParser.getDatabaseOptionsFromURI', () => { + it('creates a db options map from a query string', () => { + + for (const key in dbOptionsTest) { + const result = parser.getDatabaseOptionsFromURI(key); + + const testObj = dbOptionsTest[key]; + + for (const k in testObj) { + expect(result[k]).toEqual(testObj[k]); + } + } + + }); + + it('sets the poolSize to 10 if the it is not a number', () => { + + const result = parser.getDatabaseOptionsFromURI(`${baseURI}?poolSize=sdf`); + + expect(result.poolSize).toEqual(10); + + }); +}); diff --git a/src/Adapters/Storage/Postgres/PostgresClient.js b/src/Adapters/Storage/Postgres/PostgresClient.js new file mode 100644 index 00000000..59316b9e --- /dev/null +++ b/src/Adapters/Storage/Postgres/PostgresClient.js @@ -0,0 +1,26 @@ +const pgp = require('pg-promise')(); +const parser = require('./PostgresConfigParser'); + +export function createClient(uri, databaseOptions) { + let client; + let dbOptions = {}; + databaseOptions = databaseOptions || {}; + + if (uri) { + dbOptions = parser.getDatabaseOptionsFromURI(uri); + } + + for (const key in databaseOptions) { + dbOptions[key] = databaseOptions[key]; + } + + client = pgp(dbOptions); + + if (dbOptions.pgOptions) { + for (const key in dbOptions.pgOptions) { + client.pg.defaults[key] = dbOptions.pgOptions[key]; + } + } + + return client; +} diff --git a/src/Adapters/Storage/Postgres/PostgresConfigParser.js b/src/Adapters/Storage/Postgres/PostgresConfigParser.js new file mode 100644 index 00000000..6bcce676 --- /dev/null +++ b/src/Adapters/Storage/Postgres/PostgresConfigParser.js @@ -0,0 +1,53 @@ +const url = require('url'); + +function getDatabaseOptionsFromURI(uri) { + const databaseOptions = {}; + + const parsedURI = url.parse(uri); + const queryParams = parseQueryParams(parsedURI.query); + const authParts = parsedURI.auth ? parsedURI.auth.split(':') : []; + + databaseOptions.host = parsedURI.hostname || 'localhost'; + databaseOptions.port = parsedURI.port ? parseInt(parsedURI.port) : 5432; + databaseOptions.database = parsedURI.pathname + ? parsedURI.pathname.substr(1) + : undefined; + + databaseOptions.user = authParts.length > 0 ? authParts[0] : ''; + databaseOptions.password = authParts.length > 1 ? authParts[1] : ''; + + databaseOptions.ssl = + queryParams.ssl && queryParams.ssl.toLowerCase() === 'true' ? true : false; + databaseOptions.binary = + queryParams.binary && queryParams.binary.toLowerCase() === 'true' ? true : false; + + databaseOptions.client_encoding = queryParams.client_encoding; + databaseOptions.application_name = queryParams.application_name; + databaseOptions.fallback_application_name = queryParams.fallback_application_name; + + if (queryParams.poolSize) { + databaseOptions.poolSize = parseInt(queryParams.poolSize) || 10; + } + + return databaseOptions; +} + +function parseQueryParams(queryString) { + queryString = queryString || ''; + + return queryString + .split('&') + .reduce((p, c) => { + const parts = c.split('='); + p[decodeURIComponent(parts[0])] = + parts.length > 1 + ? decodeURIComponent(parts.slice(1).join('=')) + : ''; + return p; + }, {}); +} + +module.exports = { + parseQueryParams: parseQueryParams, + getDatabaseOptionsFromURI: getDatabaseOptionsFromURI +}; diff --git a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js index d54d987a..b2d0e471 100644 --- a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js +++ b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js @@ -1,4 +1,4 @@ -const pgp = require('pg-promise')(); +import { createClient } from './PostgresClient'; const PostgresRelationDoesNotExistError = '42P01'; const PostgresDuplicateRelationError = '42P07'; @@ -379,9 +379,10 @@ export class PostgresStorageAdapter { constructor({ uri, collectionPrefix = '', + databaseOptions }) { this._collectionPrefix = collectionPrefix; - this._client = pgp(uri); + this._client = createClient(uri, databaseOptions); } _ensureSchemaCollectionExists() { diff --git a/src/ParseServer.js b/src/ParseServer.js index 1358b5ab..fcc27de7 100644 --- a/src/ParseServer.js +++ b/src/ParseServer.js @@ -260,7 +260,8 @@ class ParseServer { case 'postgres:': return new PostgresStorageAdapter({ uri: databaseURI, - collectionPrefix + collectionPrefix, + databaseOptions }); default: return new MongoStorageAdapter({