@qvalia/knex-aws-data-api
Version:
Knex plugin that uses AWS Data API internally to execute SQL queries. Postgres & Mysql
752 lines (611 loc) • 24.9 kB
JavaScript
/* eslint-disable global-require */
/* eslint-disable no-underscore-dangle */
const rewire = require('rewire');
const {
ExecuteStatementCommand
} = require('@aws-sdk/client-rds-data');
const dataApiClient = rewire('./data-api-client');
jest.mock('@aws-sdk/client-rds-data');
describe('utility', () => {
test('error', async () => {
const error = dataApiClient.__get__('error');
const err = () => error('test error');
expect(err).toThrow('test error');
});
test('omit', async () => {
const omit = dataApiClient.__get__('omit');
const result = omit({ a: 1, b: 2, c: 3 }, ['c']);
expect(result).toEqual({ a: 1, b: 2 });
});
test('pick', async () => {
const pick = dataApiClient.__get__('pick');
const result = pick({ a: 1, b: 2, c: 3 }, ['a', 'c']);
expect(result).toEqual({ a: 1, c: 3 });
});
test('flatten', async () => {
const flatten = dataApiClient.__get__('flatten');
const result = flatten([[1, 2, 3], 4, [5, 6], 7, 8]);
expect(result).toEqual([1, 2, 3, 4, 5, 6, 7, 8]);
});
}); // end utility
describe('query parsing', () => {
describe('parseSQL', () => {
const parseSQL = dataApiClient.__get__('parseSQL');
test('string', async () => {
const result = parseSQL([`SELECT * FROM myTable`]);
expect(result).toBe('SELECT * FROM myTable');
});
test('object', async () => {
const result = parseSQL([{ sql: `SELECT * FROM myTable` }]);
expect(result).toBe('SELECT * FROM myTable');
});
test('no query (error)', async () => {
const result = () => parseSQL([]);
expect(result).toThrow(`No 'sql' statement provided.`);
});
}); // end parseSQL
describe('parseParams', () => {
const parseParams = dataApiClient.__get__('parseParams');
test('array', async () => {
const result = parseParams(['query', [1, 2]]);
expect(result).toEqual([1, 2]);
});
test('object', async () => {
const result = parseParams(['query', { a: 1, b: 2 }]);
expect(result).toEqual([{ a: 1, b: 2 }]);
});
test('array (in object)', async () => {
const result = parseParams([{ parameters: [1, 2, 3] }]);
expect(result).toEqual([1, 2, 3]);
});
test('object (in object)', async () => {
const result = parseParams([{ parameters: { a: 1, b: 2 } }]);
expect(result).toEqual([{ a: 1, b: 2 }]);
});
test('no params (empty array)', async () => {
const result = parseParams(['query']);
expect(result).toEqual([]);
});
test('no params in object (empty array)', async () => {
const result = parseParams([{}]);
expect(result).toEqual([]);
});
test('invalid type (error)', async () => {
const result = () => parseParams(['query', 'string']);
expect(result).toThrow(`'parameters' must be an object or array`);
});
test('invalid type in object (error)', async () => {
const result = () => parseParams([{ parameters: 'string' }]);
expect(result).toThrow(`'parameters' must be an object or array`);
});
}); // end parse params
}); // end query parsing
describe('query configuration parsing', () => {
test('mergeConfig', async () => {
const mergeConfig = dataApiClient.__get__('mergeConfig');
const result = mergeConfig({ secretArn: 'secretArn', resourceArn: 'resourceArn' }, { database: 'db' });
expect(result).toEqual({ secretArn: 'secretArn', resourceArn: 'resourceArn', database: 'db' });
});
describe('parseDatabase', () => {
const parseDatabase = dataApiClient.__get__('parseDatabase');
test('from config w/ transaction', async () => {
const result = parseDatabase({ database: 'db', transactionId: 'txid' });
expect(result).toBe('db');
});
test('from args', async () => {
const result = parseDatabase({ database: 'db' }, [{ database: 'db2' }]);
expect(result).toBe('db2');
});
test('from args, not string (error)', async () => {
const result = () => parseDatabase({ database: 'db' }, [{ database: 1 }]);
expect(result).toThrow(`'database' must be a string.`);
});
test('from config', async () => {
const result = parseDatabase({ database: 'db' }, [{}]);
expect(result).toBe('db');
});
test('no database provided (return undefined)', async () => {
const result = parseDatabase({}, [{}]);
expect(result).toBeUndefined();
});
}); // end parseDatabase
describe('parseHydrate', () => {
const parseHydrate = dataApiClient.__get__('parseHydrate');
test('parseHydrate - from args', async () => {
const result = parseHydrate({ hydrateColumnNames: true }, [{ hydrateColumnNames: false }]);
expect(result).toBe(false);
});
test('parseHydrate - from config', async () => {
const result = parseHydrate({ hydrateColumnNames: true }, [{}]);
expect(result).toBe(true);
});
test('parseHydrate - from args, not boolean (error)', async () => {
const result = () => parseHydrate({ hydrateColumnNames: true }, [{ hydrateColumnNames: 'false' }]);
expect(result).toThrow(`'hydrateColumnNames' must be a boolean.`);
});
});
describe('prepareParams', () => {
const prepareParams = dataApiClient.__get__('prepareParams');
test('prepareParams - omit specific args, merge others', async () => {
const result = prepareParams({ secretArn: 'secretArn', resourceArn: 'resourceArn' },
[{ hydrateColumnNames: true, parameters: [1, 2, 3], test: true }]);
expect(result).toEqual({ secretArn: 'secretArn', resourceArn: 'resourceArn', test: true });
});
test('prepareParams - no args', async () => {
const result = prepareParams({ secretArn: 'secretArn', resourceArn: 'resourceArn' }, []);
expect(result).toEqual({ secretArn: 'secretArn', resourceArn: 'resourceArn' });
});
}); // end prepareParams
}); // end query config parsing
describe('query parameter processing', () => {
test('splitParams', async () => {
const splitParams = dataApiClient.__get__('splitParams');
const result = splitParams({ param1: 'p1', param2: 'p2' });
expect(result).toEqual([
{ name: 'param1', value: 'p1' },
{ name: 'param2', value: 'p2' }
]);
});
test('normalizeParams', async () => {
const normalizeParams = dataApiClient.__get__('normalizeParams');
const result = normalizeParams([
{ name: 'param1', value: 'p1' },
{ param2: 'p2' },
[{ name: 'param3', value: 'p3' }, { param4: 'p4' }],
{ name: 'param5', value: 'p5', param6: 'p6' }
]);
expect(result).toEqual([
{ name: 'param1', value: 'p1' },
{ name: 'param2', value: 'p2' },
[
{ name: 'param3', value: 'p3' },
{ name: 'param4', value: 'p4' }
],
{ name: 'name', value: 'param5' },
{ name: 'value', value: 'p5' },
{ name: 'param6', value: 'p6' }
]);
});
describe('formatType', () => {
const formatType = dataApiClient.__get__('formatType');
test('stringValue', async () => {
const result = formatType('param', 'string val', 'stringValue');
expect(result).toEqual({ name: 'param', value: { stringValue: 'string val' } });
});
test('booleanValue', async () => {
const result = formatType('param', true, 'booleanValue');
expect(result).toEqual({ name: 'param', value: { booleanValue: true } });
});
test('longValue', async () => {
const result = formatType('param', 1234567890, 'longValue');
expect(result).toEqual({ name: 'param', value: { longValue: 1234567890 } });
});
test('existing type', async () => {
const result = formatType('param', { stringValue: 'string' }, null);
expect(result).toEqual({ name: 'param', value: { stringValue: 'string' } });
});
test('undefined (error)', async () => {
const result = () => formatType('param', 'invalid type', undefined);
expect(result).toThrow(`'param' is an invalid type`);
});
});
describe('getType', () => {
const getType = dataApiClient.__get__('getType');
test('stringValue', async () => {
const result = getType('string');
expect(result).toBe('stringValue');
});
test('booleanValue', async () => {
const result = getType(true);
expect(result).toBe('booleanValue');
});
test('longValue', async () => {
const result = getType(123456789);
expect(result).toBe('longValue');
});
test('doubleValue', async () => {
const result = getType(1234.56789);
expect(result).toBe('doubleValue');
});
test('isNull', async () => {
const result = getType(null);
expect(result).toBe('isNull');
});
test('blobValue', async () => {
const result = getType(Buffer.from('data'));
expect(result).toBe('blobValue');
});
test('invalid type (undefined)', async () => {
const result = getType([]); // use array for now
expect(result).toBeUndefined();
});
});
describe('formatParam', () => {
const formatParam = dataApiClient.__get__('formatParam');
test('stringValue', async () => {
const result = formatParam('param', 'string');
expect(result).toEqual({ name: 'param', value: { stringValue: 'string' } });
});
test('booleanValue', async () => {
const result = formatParam('param', true);
expect(result).toEqual({ name: 'param', value: { booleanValue: true } });
});
test('longValue', async () => {
const result = formatParam('param', 123456789);
expect(result).toEqual({ name: 'param', value: { longValue: 123456789 } });
});
test('doubleValue', async () => {
const result = formatParam('param', 1234.56789);
expect(result).toEqual({ name: 'param', value: { doubleValue: 1234.56789 } });
});
test('isNull', async () => {
const result = formatParam('param', null);
expect(result).toEqual({ name: 'param', value: { isNull: true } });
});
test('blobValue', async () => {
const result = formatParam('param', Buffer.from('data'));
expect(result).toEqual({
name: 'param',
value: { blobValue: Buffer.from('data') }
});
});
test('supplied type', async () => {
const result = formatParam('param', { stringValue: 'string' });
expect(result).toEqual({
name: 'param',
value: { stringValue: 'string' }
});
});
test('invalid type (error)', async () => {
const result = () => formatParam('param', []); // use array for now
expect(result).toThrow(`'param' is an invalid type`);
});
});
describe('getSqlParams', () => {
const getSqlParams = dataApiClient.__get__('getSqlParams');
test('named parameters', async () => {
const result = getSqlParams('SELECT * FROM myTable WHERE id = :id AND test = :test');
expect(result).toEqual({ id: { type: 'n_ph' }, test: { type: 'n_ph' } });
});
test('named identifiers', async () => {
const result = getSqlParams('SELECT ::name FROM myTable WHERE id = :id');
expect(result).toEqual({ id: { type: 'n_ph' }, name: { type: 'n_id' } });
});
}); // end getSqlParams
describe('processParams', () => {
const processParams = dataApiClient.__get__('processParams');
test('single param, single record', async () => {
const { processedParams, escapedSql } = processParams(
'pg',
'SELECT * FROM myTable WHERE id = :id',
{ id: { type: 'n_ph' } },
[{ name: 'id', value: 1 }]
);
expect(escapedSql).toBe('SELECT * FROM myTable WHERE id = :id');
expect(processedParams).toEqual([
{ name: 'id', value: { longValue: 1 } }
]);
});
test('mulitple params, named param, single record', async () => {
const { processedParams, escapedSql } = processParams(
'pg',
'SELECT ::columnName FROM myTable WHERE id = :id AND id2 = :id2',
{ id: { type: 'n_ph' }, id2: { type: 'n_ph' }, columnName: { type: 'n_id' } },
[
{ name: 'id', value: 1 },
{ name: 'id2', value: 2 },
{ name: 'columnName', value: 'testColumn' }
]
);
expect(escapedSql).toBe('SELECT `testColumn` FROM myTable WHERE id = :id AND id2 = :id2');
expect(processedParams).toEqual([
{ name: 'id', value: { longValue: 1 } },
{ name: 'id2', value: { longValue: 2 } }
]);
});
test('single param, multiple records', async () => {
const { processedParams, escapedSql } = processParams(
'pg',
'SELECT * FROM myTable WHERE id = :id',
{ id: { type: 'n_ph' } },
[
[{ name: 'id', value: 1 }],
[{ name: 'id', value: 2 }]
]
);
expect(escapedSql).toBe('SELECT * FROM myTable WHERE id = :id');
expect(processedParams).toEqual([
[{ name: 'id', value: { longValue: 1 } }],
[{ name: 'id', value: { longValue: 2 } }]
]);
});
test('multiple params, multiple records', async () => {
const { processedParams, escapedSql } = processParams(
'pg',
'SELECT * FROM myTable WHERE id = :id',
{ id: { type: 'n_ph' }, id2: { type: 'n_ph' } },
[
[{ name: 'id', value: 1 }, { name: 'id2', value: 2 }],
[{ name: 'id', value: 2 }, { name: 'id2', value: 3 }]
]
);
expect(escapedSql).toBe('SELECT * FROM myTable WHERE id = :id');
expect(processedParams).toEqual([
[{ name: 'id', value: { longValue: 1 } }, { name: 'id2', value: { longValue: 2 } }],
[{ name: 'id', value: { longValue: 2 } }, { name: 'id2', value: { longValue: 3 } }]
]);
});
test('mulitple params, named params, multiple records', async () => {
const { processedParams, escapedSql } = processParams(
'pg',
'SELECT ::columnName FROM myTable WHERE id = :id AND id2 = :id2',
{ id: { type: 'n_ph' }, id2: { type: 'n_ph' }, columnName: { type: 'n_id' } },
[
[
{ name: 'id', value: 1 },
{ name: 'id2', value: 2 },
{ name: 'columnName', value: 'testColumn' }
],
[
{ name: 'id', value: 2 },
{ name: 'id2', value: 3 },
{ name: 'columnName', value: 'testColumnx' } // ignored
]
]
);
expect(escapedSql).toBe('SELECT `testColumn` FROM myTable WHERE id = :id AND id2 = :id2');
expect(processedParams).toEqual([
[
{ name: 'id', value: { longValue: 1 } },
{ name: 'id2', value: { longValue: 2 } }
],
[
{ name: 'id', value: { longValue: 2 } },
{ name: 'id2', value: { longValue: 3 } }
]
]);
});
test('typecasting params', async () => {
const { processedParams, escapedSql } = processParams(
'pg',
'INSERT INTO users(id, name, meta) VALUES(:id, :name, :meta)',
{ id: { type: 'n_ph' }, name: { type: 'n_ph' }, meta: { type: 'n_ph' } },
[
{ name: 'id', value: '0bb99248-2e7d-4007-a4b2-579b00649ce1', cast: 'uuid' },
{ name: 'name', value: 'Test' },
{ name: 'meta', value: '{"extra": true}', cast: 'jsonb' }
]
);
expect(escapedSql).toBe('INSERT INTO users(id, name, meta) VALUES(:id::uuid, :name, :meta::jsonb)');
expect(processedParams).toEqual([
{ name: 'id', value: { stringValue: '0bb99248-2e7d-4007-a4b2-579b00649ce1' } },
{ name: 'name', value: { stringValue: 'Test' } },
{ name: 'meta', value: { stringValue: '{"extra": true}' } }
]);
});
}); // end processParams
});
describe('querying', () => {
describe('query', () => {
const query = dataApiClient.__get__('query');
let parameters = {};
jest.spyOn(ExecuteStatementCommand.prototype, 'constructor');
const config = {
secretArn: 'secretArn',
resourceArn: 'resourceArn',
database: 'db',
RDS: {
send: (awsExecuteObject) => {
// capture the parameters for testing
// ExecuteStatementCommand contains input property which has the configs
parameters = awsExecuteObject.input;
return Promise.resolve(require('../../test/test-data/data-api-client/sample-query-response.json'));
}
}
};
test('simple query', async () => {
const result = await query(config, 'SELECT * FROM table WHERE id < :id', { id: 3 });
expect(result).toEqual({
records: [
[1, 'Category 1', null, '2019-11-12 22:00:11', '2019-11-12 22:15:25', null],
[2, 'Category 2', 'Description of Category 2', '2019-11-12 22:17:11', '2019-11-12 22:21:36', null]
]
});
expect(parameters).toEqual({
secretArn: 'secretArn',
resourceArn: 'resourceArn',
database: 'db',
sql: 'SELECT * FROM table WHERE id < :id',
parameters: [{ name: 'id', value: { longValue: 3 } }]
});
});
}); // end query
describe('formatRecords', () => {
const formatRecords = dataApiClient.__get__('formatRecords');
test('with columnMetadata', async () => {
const { records, columnMetadata } = require('../../test/test-data/data-api-client/sample-query-response.json');
const result = formatRecords(records, columnMetadata, true);
expect(result).toEqual([
{
created: '2019-11-12 22:00:11',
deleted: null,
description: null,
id: 1,
modified: '2019-11-12 22:15:25',
name: 'Category 1'
},
{
created: '2019-11-12 22:17:11',
deleted: null,
description: 'Description of Category 2',
id: 2,
modified: '2019-11-12 22:21:36',
name: 'Category 2'
}
]);
});
test('without columnMetadata', async () => {
const { records } = require('../../test/test-data/data-api-client/sample-query-response.json');
const result = formatRecords(records, false);
expect(result).toEqual([
[1, 'Category 1', null, '2019-11-12 22:00:11', '2019-11-12 22:15:25', null],
[2, 'Category 2', 'Description of Category 2', '2019-11-12 22:17:11', '2019-11-12 22:21:36', null]
]);
});
}); // end formatRecords
describe('formatUpdateResults', () => {
const formatUpdateResults = dataApiClient.__get__('formatUpdateResults');
test('with insertIds', async () => {
const { updateResults } = require('../../test/test-data/data-api-client/sample-batch-insert-response.json');
const result = formatUpdateResults(updateResults);
expect(result).toEqual([
{ insertId: 316 },
{ insertId: 317 }
]);
});
test('without insertIds', async () => {
const { updateResults } = require('../../test/test-data/data-api-client/sample-batch-update-response.json');
const result = formatUpdateResults(updateResults);
expect(result).toEqual([
{},
{}
]);
});
});
describe('formatResults', () => {
const formatResults = dataApiClient.__get__('formatResults');
test('select (hydrate)', async () => {
const response = require('../../test/test-data/data-api-client/sample-query-response.json');
const result = formatResults(response, true, false);
expect(result).toEqual({
records: [
{
created: '2019-11-12 22:00:11',
deleted: null,
description: null,
id: 1,
modified: '2019-11-12 22:15:25',
name: 'Category 1'
},
{
created: '2019-11-12 22:17:11',
deleted: null,
description: 'Description of Category 2',
id: 2,
modified: '2019-11-12 22:21:36',
name: 'Category 2'
}
]
});
});
test('select (hydrate) with date deserialization', async () => {
const response = require('../../test/test-data/data-api-client/sample-query-response.json');
const result = formatResults(response, true, false, { deserializeDate: true });
expect(result).toEqual({
records: [
{
created: new Date('2019-11-12T22:00:11Z'),
deleted: null,
description: null,
id: 1,
modified: new Date('2019-11-12T22:15:25Z'),
name: 'Category 1'
},
{
created: new Date('2019-11-12T22:17:11Z'),
deleted: null,
description: 'Description of Category 2',
id: 2,
modified: new Date('2019-11-12T22:21:36Z'),
name: 'Category 2'
}
]
});
});
test('select (no hydrate)', async () => {
const response = require('../../test/test-data/data-api-client/sample-query-response.json');
const result = formatResults(response, false, false);
expect(result).toEqual({
records: [
[1, 'Category 1', null, '2019-11-12 22:00:11', '2019-11-12 22:15:25', null],
[2, 'Category 2', 'Description of Category 2', '2019-11-12 22:17:11', '2019-11-12 22:21:36', null]
]
});
});
test('select (with metadata)', async () => {
const response = require('../../test/test-data/data-api-client/sample-query-response.json');
const { columnMetadata } = require('../../test/test-data/data-api-client/sample-query-response.json');
const result = formatResults(response, false, true);
expect(result).toEqual({
columnMetadata,
records: [
[1, 'Category 1', null, '2019-11-12 22:00:11', '2019-11-12 22:15:25', null],
[2, 'Category 2', 'Description of Category 2', '2019-11-12 22:17:11', '2019-11-12 22:21:36', null]
]
});
});
test('select (with date deserialization to UTC)', async () => {
const response = require('../../test/test-data/data-api-client/sample-query-response.json');
const result = formatResults(response, false, false, { deserializeDate: true });
expect(result).toEqual({
records: [
[1, 'Category 1', null, new Date('2019-11-12T22:00:11.000Z'), new Date('2019-11-12T22:15:25.000Z'), null],
[2, 'Category 2', 'Description of Category 2', new Date('2019-11-12T22:17:11.000Z'), new Date('2019-11-12T22:21:36.000Z'), null]
]
});
});
test('select (with date deserialization to local TZ)', async () => {
const response = require('../../test/test-data/data-api-client/sample-query-response.json');
const result = formatResults(response, false, false, { deserializeDate: true, treatAsLocalDate: true });
expect(result).toEqual({
records: [
[1, 'Category 1', null, new Date('2019-11-12 22:00:11'), new Date('2019-11-12 22:15:25'), null],
[2, 'Category 2', 'Description of Category 2', new Date('2019-11-12 22:17:11'), new Date('2019-11-12 22:21:36'), null]
]
});
});
test('update', async () => {
const response = require('../../test/test-data/data-api-client/sample-update-response.json');
const result = formatResults(response, false, false);
expect(result).toEqual({
numberOfRecordsUpdated: 1
});
});
test('delete', async () => {
const response = require('../../test/test-data/data-api-client/sample-delete-response.json');
const result = formatResults(response, false, false);
expect(result).toEqual({
numberOfRecordsUpdated: 1
});
});
test('insert', async () => {
const response = require('../../test/test-data/data-api-client/sample-insert-response.json');
const result = formatResults(response, false, false);
expect(result).toEqual({
insertId: 315,
numberOfRecordsUpdated: 1
});
});
test('batch update', async () => {
const response = require('../../test/test-data/data-api-client/sample-batch-update-response.json');
const result = formatResults(response, false, false);
expect(result).toEqual({
updateResults: [{}, {}]
});
});
test('batch delete', async () => {
const response = require('../../test/test-data/data-api-client/sample-batch-delete-response.json');
const result = formatResults(response, false, false);
expect(result).toEqual({
updateResults: [{}, {}]
});
});
test('batch insert', async () => {
const response = require('../../test/test-data/data-api-client/sample-batch-insert-response.json');
const result = formatResults(response, false, false);
expect(result).toEqual({
updateResults: [{ insertId: 316 }, { insertId: 317 }]
});
});
});
});