simple-oracledb
Extend capabilities of oracledb with simplified API for quicker development.
Overview
This library enables to modify the oracledb main object, oracledb pool and oracledb connection of the official oracle node.js driver.
The main goal is to provide an extended oracledb connection which provides more functionality for most use cases.
The new functionality aim is to be simpler and more straightforward to enable quicker development.
Usage
In order to use this library, you need to either extend the main oracledb object as follows:
var oracledb = require('oracledb');
var SimpleOracleDB = require('simple-oracledb');
SimpleOracleDB.extend(oracledb);
oracledb.getConnection(function onConnection(error, connection) {
if (error) {
} else {
connection.query(...);
}
});
Another option is to modify your oracledb pool instance (in case the pool was created outside your code and
out of your control), as follows:
var SimpleOracleDB = require('simple-oracledb');
function myFunction(pool) {
SimpleOracleDB.extend(pool);
pool.getConnection(function onConnection(error, connection) {
if (error) {
} else {
connection.query(...);
}
});
}
One last option is to modify your oracledb connection instance (in case the connection was created outside your code
and out of your control), as follows:
var SimpleOracleDB = require('simple-oracledb');
function doSomething(connection, callback) {
SimpleOracleDB.extend(connection);
connection.query(...);
}
Class: OracleDB
Event: 'pool-created'
This events is triggered when a pool is created.
Event: 'pool-released'
This events is triggered after a pool is released.
Event: 'connection-created'
- connection - The connection instance
This events is triggered when a connection is created via oracledb.
Event: 'connection-released'
- connection - The connection instance
This events is triggered when a connection is released successfully.
'oracledb.createPool(poolAttributes, [callback]) ⇒ [Promise]'
This function modifies the existing oracledb.createPool function by enhancing the returned pool to support retry in the getConnection function.
The pool.getConnection will retry configurable amount of times with configurable interval between attempts to return a connection in the getConnection function.
In case all attempts fail, the getConnection callback will receive the error object of the last attempt.
oracledb.createPool({
retryCount: 5,
retryInterval: 500,
runValidationSQL: true,
validationSQL: 'SELECT 1 FROM DUAL',
}, function onPoolCreated(error, pool) {
});
'oracledb.run(connectionAttributes, action, [callback]) ⇒ [Promise]'
This function invokes the provided action (function) with a valid connection object and a callback.
The action can use the provided connection to run any connection operation/s (execute/query/transaction/...) and after finishing it
must call the callback with an error (if any) and result.
This function will ensure the connection is released properly and only afterwards will call the provided callback with the action error/result.
This function basically will remove the need of caller code to get and release a connection and focus on the actual database operation logic.
It is recommanded to create a pool and use the pool.run instead of oracledb.run as this function will create a new connection (and release it) for each invocation,
on the other hand, pool.run will reuse pool managed connections which will result in improved performance.
Example
oracledb.run({
user: process.env.ORACLE_USER,
password: process.env.ORACLE_PASSWORD,
connectString: process.env.ORACLE_CONNECTION_STRING
}, function onConnection(connection, callback) {
connection.query('SELECT department_id, department_name FROM departments WHERE manager_id < :id', [110], callback);
}, function onActionDone(error, result) {
});
oracledb.run({
user: process.env.ORACLE_USER,
password: process.env.ORACLE_PASSWORD,
connectString: process.env.ORACLE_CONNECTION_STRING
}, function (connection, callback) {
connection.transaction([
function firstAction(callback) {
connection.insert(...., callback);
},
function secondAction(callback) {
connection.update(...., callback);
}
], {
sequence: true
}, callback);
}, function onActionDone(error, result) {
});
Class: Pool
Event: 'connection-created'
- connection - The connection instance
This events is triggered when a connection is created via pool.
Event: 'connection-released'
- connection - The connection instance
This events is triggered when a connection is released successfully.
Event: 'release'
This events is triggered after the pool is released successfully.
'pool.getConnection([callback]) ⇒ [Promise]'
Wraps the original oracledb getConnection in order to provide an extended connection object.
In addition, this function will attempt to fetch a connection from the pool and in case of any error will reattempt for a configurable amount of times.
It will also ensure the provided connection is valid by running a test SQL and if validation fails, it will fetch another connection (continue to reattempt).
See getConnection for official API details.
See createPool for extended createPool API details.
Example
oracledb.createPool({
retryCount: 5,
retryInterval: 500,
runValidationSQL: true,
validationSQL: 'SELECT 1 FROM DUAL',
}, function onPoolCreated(error, pool) {
pool.getConnection(function onConnection(poolError, connection) {
});
});
'pool.run(action, [options], [callback]) ⇒ [Promise]'
This function invokes the provided action (function) with a valid connection object and a callback.
The action can use the provided connection to run any connection operation/s (execute/query/transaction/...) and after finishing it
must call the callback with an error (if any) and result.
The pool will ensure the connection is released properly and only afterwards will call the provided callback with the action error/result.
This function basically will remove the need of caller code to get and release a connection and focus on the actual database operation logic.
Example
pool.run(function (connection, callback) {
connection.query('SELECT department_id, department_name FROM departments WHERE manager_id < :id', [110], callback);
}, function onActionDone(error, result) {
});
pool.run(function (connection, callback) {
connection.transaction([
function firstAction(callback) {
connection.insert(...., callback);
},
function secondAction(callback) {
connection.update(...., callback);
}
], {
sequence: true
}, callback);
}, {
ignoreReleaseErrors: false
}, function onActionDone(error, result) {
});
'pool.terminate([callback]) ⇒ [Promise]'
'pool.close([callback]) ⇒ [Promise]'
This function modifies the existing pool.terminate function by enabling the input
callback to be an optional parameter.
Since there is no real way to release the pool that fails to be terminated, all that you can do in the callback
is just log the error and continue.
Therefore this function allows you to ignore the need to pass a callback and makes it as an optional parameter.
The pool.terminate also has an alias pool.close for consistent close function naming to all relevant objects.
Example
pool.terminate();
pool.terminate(function onTerminate(error) {
if (error) {
}
});
pool.close();
Class: Connection
Event: 'release'
This events is triggered when the connection is released successfully.
'connection.query(sql, [bindParams], [options], [callback]) ⇒ [ResultSetReadStream] | [Promise]'
Provides simpler interface than the original oracledb connection.execute function to enable simple query invocation.
The callback output will be an array of objects, each object holding a property for each field with the actual value.
All LOBs will be read and all rows will be fetched.
This function is not recommended for huge results sets or huge LOB values as it will consume a lot of memory.
The function arguments used to execute the 'query' are exactly as defined in the oracledb connection.execute function.
Example
connection.query('SELECT department_id, department_name FROM departments WHERE manager_id < :id', [110], function onResults(error, results) {
if (error) {
} else {
console.log(results[3].DEPARTMENT_ID);
}
});
connection.query('SELECT * FROM departments WHERE manager_id > :id', [110], {
splitResults: true,
bulkRowsAmount: 100
}, function onResults(error, results) {
if (error) {
} else if (results.length) {
} else {
}
});
var stream = connection.query('SELECT * FROM departments WHERE manager_id > :id', [110], {
streamResults: true
});
stream.on('data', function (row) {
if (row.MY_ID === 800) {
stream.close();
}
});
stream.on('metadata', function (metaData) {
console.log(metaData);
});
'connection.insert(sql, [bindParams], [options], [callback]) ⇒ [Promise]'
Provides simpler interface than the original oracledb connection.execute function to enable simple insert invocation with LOB support.
The callback output will be the same as oracledb connection.execute.
All LOBs will be written to the DB via streams and only after all LOBs are written the callback will be called.
The function arguments used to execute the 'insert' are exactly as defined in the oracledb connection.execute function, however the options are mandatory.
Example
connection.insert('INSERT INTO mylobs (id, clob_column1, blob_column2) VALUES (:id, EMPTY_CLOB(), EMPTY_BLOB())', {
id: 110,
clobText1: 'some long clob string',
blobBuffer2: new Buffer('some blob content, can be binary...')
}, {
autoCommit: true,
lobMetaInfo: {
clob_column1: 'clobText1',
blob_column2: 'blobBuffer2'
}
}, function onResults(error, output) {
});
connection.insert('INSERT INTO mylobs (id, clob_column1, blob_column2) VALUES (:myid, EMPTY_CLOB(), EMPTY_BLOB())', {
myid: {
type: oracledb.NUMBER,
dir: oracledb.BIND_INOUT,
val: 1234
},
clobText1: 'some long clob string',
blobBuffer2: new Buffer('some blob content, can be binary...')
}, {
autoCommit: true,
lobMetaInfo: {
clob_column1: 'clobText1',
blob_column2: 'blobBuffer2'
},
returningInfo: {
id: 'myid'
}
}, function onResults(error, output) {
});
'connection.update(sql, [bindParams], [options], [callback]) ⇒ [Promise]'
Provides simpler interface than the original oracledb connection.execute function to enable simple update invocation with LOB support.
The callback output will be the same as oracledb connection.execute.
All LOBs will be written to the DB via streams and only after all LOBs are written the callback will be called.
The function arguments used to execute the 'update' are exactly as defined in the oracledb connection.execute function, however the options are mandatory.
Example
connection.update('UPDATE mylobs SET name = :name, clob_column1 = EMPTY_CLOB(), blob_column2 = EMPTY_BLOB() WHERE id = :id', {
id: 110,
name: 'My Name',
clobText1: 'some long clob string',
blobBuffer2: new Buffer('some blob content, can be binary...')
}, {
autoCommit: true,
lobMetaInfo: {
clob_column1: 'clobText1',
blob_column2: 'blobBuffer2'
}
}, function onResults(error, output) {
});
'connection.queryJSON(sql, [bindParams], [options], [callback]) ⇒ [Promise]'
This function will invoke the provided SQL SELECT and return a results object with the returned row count and the JSONs.
The json property will hold a single JSON object in case the returned row count is 1, and an array of JSONs in case the row count is higher.
The query expects that only 1 column is fetched and if more are detected in the results, this function will return an error in the callback.
The function arguments used to execute the 'queryJSON' are exactly as defined in the oracledb connection.execute function.
Example
connection.queryJSON('SELECT JSON_DATA FROM APP_CONFIG WHERE ID > :id', [110], function onResults(error, results) {
if (error) {
} else if (results.rowCount === 1) {
console.log(results.json);
} else if (results.rowCount > 1) {
results.json.forEach(function printJSON(json) {
console.log(json);
});
} else {
console.log('Did not find any results');
}
});
'connection.batchInsert(sql, bindParamsArray, options, [callback]) ⇒ [Promise]'
Enables to run an INSERT SQL statement multiple times for each of the provided bind params.
This allows to insert to same table multiple different rows with one single call.
The callback output will be an array of objects of same as oracledb connection.execute (per row).
All LOBs for all rows will be written to the DB via streams and only after all LOBs are written the callback will be called.
The function arguments used to execute the 'insert' are exactly as defined in the oracledb connection.execute function, however the options are mandatory and
the bind params is now an array of bind params (one per row).
Example
connection.batchInsert('INSERT INTO mylobs (id, clob_column1, blob_column2) VALUES (:id, EMPTY_CLOB(), EMPTY_BLOB())', [
{
id: 110,
clobText1: 'some long clob string',
blobBuffer2: new Buffer('some blob content, can be binary...')
},
{
id: 111,
clobText1: 'second row',
blobBuffer2: new Buffer('second rows')
}
], {
autoCommit: true,
lobMetaInfo: {
clob_column1: 'clobText1',
blob_column2: 'blobBuffer2'
}
}, function onResults(error, output) {
});
'connection.batchUpdate(sql, bindParamsArray, options, [callback]) ⇒ [Promise]'
Enables to run an UPDATE SQL statement multiple times for each of the provided bind params.
This allows to update to same table multiple different rows with one single call.
The callback output will be an array of objects of same as oracledb connection.execute (per row).
All LOBs for all rows will be written to the DB via streams and only after all LOBs are written the callback will be called.
The function arguments used to execute the 'update' are exactly as defined in the oracledb connection.execute function, however the options are mandatory and
the bind params is now an array of bind params (one per row).
Example
connection.batchUpdate('UPDATE mylobs SET name = :name, clob_column1 = EMPTY_CLOB(), blob_column2 = EMPTY_BLOB() WHERE id = :id', [
{
id: 110,
clobText1: 'some long clob string',
blobBuffer2: new Buffer('some blob content, can be binary...')
},
{
id: 111,
clobText1: 'second row',
blobBuffer2: new Buffer('second rows')
}
], {
autoCommit: true,
lobMetaInfo: {
clob_column1: 'clobText1',
blob_column2: 'blobBuffer2'
}
}, function onResults(error, output) {
});
'connection.transaction(actions, [options], [callback]) ⇒ [Promise]'
Enables to run multiple oracle operations in a single transaction.
This function basically allows to automatically commit or rollback once all your actions are done.
Actions are basically javascript functions which get a callback when invoked, and must call that callback with error or result.
All provided actions are executed in sequence unless options.sequence=false is provided (parallel invocation is only for IO operations apart of the oracle driver as the driver will queue operations on same connection).
Once all actions are done, in case of any error in any action, a rollback will automatically get invoked, otherwise a commit will be invoked.
Once the rollback/commit is done, the provided callback will be invoked with the error (if any) and results of all actions.
When calling any connection operation (execute, insert, update, ...) the connection will automatically set the autoCommit=false and will ignore the value provided.
This is done to prevent commits in the middle of the transaction.
In addition, you can not start a transaction while another transaction is in progress.
Example
connection.transaction([
function insertSomeRows(callback) {
connection.insert(...., function (error, results) {
connection.insert(...., callback);
});
},
function insertSomeMoreRows(callback) {
connection.insert(...., callback);
},
function doSomeUpdates(callback) {
connection.update(...., callback);
},
function runBatchUpdates(callback) {
connection.batchUpdate(...., callback);
}
], {
sequence: false
}, function onTransactionResults(error, output) {
});
connection.transaction([
function firstAction(callback) {
connection.insert(...., callback);
},
function secondAction(callback) {
connection.update(...., callback);
}
], {
sequence: true
}, function onTransactionResults(error, output) {
});
'connection.run(actions, [options], [callback]) ⇒ [Promise]'
Enables to run multiple oracle operations in sequence or parallel.
Actions are basically javascript functions which get a callback when invoked, and must call that callback with error or result.
All provided actions are executed in sequence unless options.sequence=false is provided (parallel invocation is only for IO operations apart of the oracle driver as the driver will queue operations on same connection).
This function is basically the same as connection.transaction with few exceptions
- This function will not auto commit/rollback or disable any commits/rollbacks done by the user
- You can invoke connection.run inside connection.run as many times as needed (for example if you execute connection.run with option.sequence=false meaning parallel and inside invoke connection.run with option.sequence=true for a subset of operations)
Example
connection.run([
function insertSomeRows(callback) {
connection.insert(...., function (error, results) {
connection.insert(...., callback);
});
},
function insertSomeMoreRows(callback) {
connection.insert(...., callback);
},
function doSomeUpdates(callback) {
connection.update(...., callback);
},
function runBatchUpdates(callback) {
connection.batchUpdate(...., callback);
}
], {
sequence: false
}, function onActionsResults(error, output) {
});
connection.run([
function firstAction(callback) {
connection.insert(...., callback);
},
function secondAction(callback) {
connection.update(...., callback);
}
], {
sequence: true
}, function onActionsResults(error, output) {
});
connection.run([
function firstAction(callback) {
connection.insert(...., callback);
},
function secondAction(callback) {
connection.update(...., callback);
},
function subsetInParallel(callback) {
connection.run([
function insertSomeRows(subsetCallback) {
connection.insert(...., function (error, results) {
connection.insert(...., subsetCallback);
});
},
function insertSomeMoreRows(subsetCallback) {
connection.insert(...., subsetCallback);
},
function doSomeUpdates(subsetCallback) {
connection.update(...., subsetCallback);
},
function runBatchUpdates(subsetCallback) {
connection.batchUpdate(...., subsetCallback);
}
], {
sequence: false
}, callback);
}
], {
sequence: true
}, function onActionsResults(error, output) {
});
'connection.executeFile(file, [options], [callback]) ⇒ [Promise]'
Reads the sql string from the provided file and executes it.
The file content must be a single valid SQL command string.
This function is basically a quick helper to reduce the coding needed to read the sql file.
Example
connection.executeFile('./populate_table.sql', function onResults(error, results) {
if (error) {
} else {
}
});
'connection.release([options], [callback]) ⇒ [Promise]'
'connection.close([options], [callback]) ⇒ [Promise]'
This function modifies the existing connection.release function by enabling the input
callback to be an optional parameter and providing ability to auto retry in case of any errors during release.
The connection.release also has an alias connection.close for consistent close function naming to all relevant objects.
Example
connection.release();
connection.release(function onRelease(error) {
if (error) {
}
});
connection.release({
retryCount: 20,
retryInterval: 1000
});
connection.release({
retryCount: 10,
retryInterval: 250,
force: true
}, function onRelease(error) {
if (error) {
}
});
connection.close({
retryCount: 10,
retryInterval: 250
}, function onRelease(error) {
if (error) {
}
});
'connection.rollback([callback]) ⇒ [Promise]'
This function modifies the existing connection.rollback function by enabling the input
callback to be an optional parameter.
If rollback fails, you can't really rollback again the data, so the callback is not always needed.
Therefore this function allows you to ignore the need to pass a callback and makes it as an optional parameter.
Example
connection.rollback();
connection.rollback(function onRollback(error) {
if (error) {
}
});
Class: SimpleOracleDB
Event: 'pool-created'
This events is triggered when a pool is created.
Event: 'pool-released'
This events is triggered after a pool is released.
Event: 'connection-created'
- connection - The connection instance
This events is triggered when a connection is created via oracledb.
Event: 'connection-released'
- connection - The connection instance
This events is triggered when a connection is released successfully.
'SimpleOracleDB.diagnosticInfo'
The pool/connection diagnostics info.
This includes info of all live pools (including live time and create time) and all live connections (including parent pool if any, live time, create time and last SQL)
'SimpleOracleDB.enableDiagnosticInfo'
True if the monitoring is enabled and it will listen and store pool/connection diagnostics information.
By default this is set to false.
'SimpleOracleDB.addExtension(type, name, extension, [options]) ⇒ boolean'
Adds an extension to all newly created objects of the requested type.
An extension, is a function which will be added to any pool or connection instance created after the extension was added.
This function enables external libraries to further extend oracledb using a very simple API and without the need to wrap the pool/connection creation functions.
Extension functions automatically get promisified unless specified differently in the optional options.
Example
SimpleOracleDB.addExtension('connection', 'myConnFunc', function (myParam1, myParam2, callback) {
callback();
});
connection.myConnFunc('test', 123, function () {
});
var promise = connection.myConnFunc('test', 123);
promise.then(function () {
}).catch(function (error) {
});
SimpleOracleDB.addExtension('pool', 'myPoolFunc', function () {
});
pool.myPoolFunc();
An example of an existing extension can be found at: oracledb-upsert which adds the connection.upsert (insert/update) functionality.
'connection.upsert(sqls, bindParams, [options], [callback]) ⇒ [Promise]'
See oracledb-upsert for more info.
**The rest of the API is the same as defined in the oracledb library: https://github.com/oracle/node-oracledb/blob/master/doc/api.md**
Debug
In order to turn on debug messages, use the standard nodejs NODE_DEBUG environment variable.
NODE_DEBUG=simple-oracledb
Installation
In order to use this library, just run the following npm install command:
npm install --save simple-oracledb
This library doesn't define oracledb as a dependency and therefore it is not installed when installing simple-oracledb.
You should define oracledb in your package.json and install it based on the oracledb installation instructions found at: installation guide
Known Issues
- oracledb version 1.7.0 breaks the API and prevents the library from being extended. This was fixed in oracledb 1.7.1 (oracledb case)
API Documentation
See full docs at: API Docs
Contributing
See contributing guide
Release History
Date | Version | Description |
---|
2016-11-26 | v1.1.46 | Maintenance |
2016-11-15 | v1.1.41 | Added connection.executeFile to read SQL statement from file and execute it |
2016-11-05 | v1.1.40 | Maintenance |
2016-10-07 | v1.1.26 | Added oracledb.run |
2016-10-06 | v1.1.25 | Maintenance |
2016-08-15 | v1.1.2 | Added 'metadata' event for connection.query with streaming |
2016-08-15 | v1.1.1 | Maintenance |
2016-08-10 | v1.1.0 | Breaking change connection.run and connection.transaction default is now sequence instead of parallel |
2016-08-09 | v1.0.2 | Added connection.run |
2016-08-09 | v1.0.1 | Maintenance |
2016-08-07 | v0.1.98 | NODE_DEBUG=simple-oracledb will now also log all SQL statements and bind params for the connection.execute function |
2016-08-05 | v0.1.97 | Maintenance |
2016-08-05 | v0.1.96 | Extensions are now automatically promisified |
2016-08-05 | v0.1.95 | Added promise support for all library APIs |
2016-08-03 | v0.1.94 | Maintenance |
2016-07-26 | v0.1.84 | Add integration test via docker |
2016-07-24 | v0.1.83 | Add support for node-oracledb promise |
2016-07-23 | v0.1.82 | Maintenance |
2016-07-17 | v0.1.80 | Add support for node-oracledb promise |
2016-07-14 | v0.1.79 | Fixed possible max stack size error |
2016-07-13 | v0.1.78 | Maintenance |
2016-05-01 | v0.1.57 | Added the new monitor (SimpleOracleDB.diagnosticInfo and SimpleOracleDB.enableDiagnosticInfo) and SimpleOracleDB is now an event emitter |
2016-04-27 | v0.1.54 | Maintenance |
2016-03-31 | v0.1.51 | Added new stream.close function to stop streaming data and free the connection for more operations |
2016-03-09 | v0.1.50 | Maintenance |
2016-03-03 | v0.1.40 | Connection and Pool are now event emitters |
2016-03-02 | v0.1.39 | Maintenance |
2016-03-02 | v0.1.38 | Added new force option for connection.release/close |
2016-02-28 | v0.1.37 | Added SimpleOracleDB.addExtension which allows to further extend oracledb |
2016-02-28 | v0.1.36 | Maintenance |
2016-02-22 | v0.1.32 | Added new pool.run operation |
2016-02-21 | v0.1.31 | Maintenance |
2016-02-16 | v0.1.29 | new optional options.returningInfo to insert/update/batch to enable to modify the returning/into clause when using LOBs |
2016-02-16 | v0.1.28 | Maintenance |
2016-02-12 | v0.1.26 | Added sequence option for connection.transaction and added pool.close=pool.terminate, connection.close=connection.release aliases |
2016-02-11 | v0.1.25 | Maintenance |
2016-02-10 | v0.1.23 | Adding debug logs via NODE_DEBUG=simple-oracledb |
2016-02-09 | v0.1.22 | Maintenance |
2016-02-09 | v0.1.20 | connection.release now supports retry options |
2016-02-02 | v0.1.19 | Maintenance |
2016-01-22 | v0.1.18 | Fixed missing call to resultset.close after done reading |
2016-01-20 | v0.1.17 | Maintenance |
2016-01-12 | v0.1.8 | Avoid issues with oracledb stream option which is based on this library |
2016-01-07 | v0.1.7 | connection.query with streamResults=true returns a readable stream |
2015-12-30 | v0.1.6 | connection.transaction disables commit/rollback while running |
2015-12-29 | v0.1.5 | Maintenance |
2015-12-29 | v0.1.4 | Added connection.transaction |
2015-12-29 | v0.1.3 | Added connection.batchUpdate |
2015-12-22 | v0.1.2 | Added streaming of query results with new option streamResults=true |
2015-12-21 | v0.1.1 | Rename streamResults to splitResults |
2015-12-21 | v0.0.36 | Maintenance |
2015-12-21 | v0.0.35 | New bulkRowsAmount option to manage query resultset behaviour |
2015-12-21 | v0.0.34 | Added splitting of query results into bulks with new option splitResults=true |
2015-12-17 | v0.0.33 | Maintenance |
2015-12-08 | v0.0.24 | Added pool.getConnection connection validation via running SQL test command |
2015-11-30 | v0.0.23 | Maintenance |
2015-11-17 | v0.0.17 | Added pool.getConnection automatic retry |
2015-11-15 | v0.0.16 | Added connection.batchInsert and connection.rollback |
2015-11-05 | v0.0.15 | Maintenance |
2015-10-20 | v0.0.10 | Added connection.queryJSON |
2015-10-19 | v0.0.9 | autoCommit support when doing INSERT/UPDATE with LOBs |
2015-10-19 | v0.0.7 | Added pool.terminate |
2015-10-19 | v0.0.6 | Maintenance |
2015-10-18 | v0.0.5 | Added connection.update |
2015-10-18 | v0.0.4 | Added connection.insert |
2015-10-16 | v0.0.3 | Maintenance |
2015-10-15 | v0.0.1 | Initial release. |
License
Developed by Sagie Gur-Ari and licensed under the Apache 2 open source license.