autolevel.js

Automatically use the right abstract-leveldown module for your configuration
git clone git://git.finwo.net/lib/autolevel.js
Log | Files | Refs | README | LICENSE

commit 069fb113e8e2224af05c7123423167513ea3f318
parent e68e95084da05221f4a04775655ddedb75a265f8
Author: finwo <finwo@pm.me>
Date:   Mon,  5 Nov 2018 13:33:28 +0100

Using adapters as plugins now

Diffstat:
M.gitignore | 6+++++-
Asrc/adapter.js | 122+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/has-module.js | 15+++++++++++++++
Asrc/has-module.test.js | 41+++++++++++++++++++++++++++++++++++++++++
Msrc/index.js | 24++----------------------
Msrc/index.test.js | 79+++++++++++++++++++++++++++++++------------------------------------------------
Dsrc/mem.js | 21---------------------
Dsrc/mem.test.js | 35-----------------------------------
Dsrc/mongodb.js | 20--------------------
Dsrc/mongodb.test.js | 39---------------------------------------
Dsrc/plain.js | 35-----------------------------------
Dsrc/plain.test.js | 39---------------------------------------
Dsrc/sql.js | 27---------------------------
Dsrc/sql.test.js | 39---------------------------------------
Asrc/to-absolute-path.js | 13+++++++++++++
Asrc/to-absolute-path.test.js | 34++++++++++++++++++++++++++++++++++
Mupdate.sh | 1+
17 files changed, 264 insertions(+), 326 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -1,5 +1,6 @@ # Created by https://www.gitignore.io/api/osx,linux,windows,node +# Edit at https://www.gitignore.io/?templates=osx,linux,windows,node ### Linux ### *~ @@ -91,6 +92,9 @@ typings/ # Serverless directories .serverless +# FuseBox cache +.fusebox/ + ### OSX ### # General .DS_Store @@ -144,7 +148,7 @@ $RECYCLE.BIN/ # Windows shortcuts *.lnk - # End of https://www.gitignore.io/api/osx,linux,windows,node .idea/ .gtm/ +/data/ diff --git a/src/adapter.js b/src/adapter.js @@ -0,0 +1,122 @@ +// Our own dependencies +const hasModule = require('./has-module'), + toAbsolutePath = require('./to-absolute-path'); + +// Adapter descriptors +let adapters = [ + { + name : ['ram', 'mem', 'memory'], + backend : 'memdown', + dependencies: ['levelup', 'memdown'], + options : { + path: 'omit' + } + }, { + name : ['dir', 'leveldb', 'level'], + backend : 'leveldown', + dependencies: ['levelup', 'leveldown'], + options : { + path: 'absolute' + } + }, { + name : ['mongo', 'mongodb'], + backend : 'mongodown', + dependencies: ['levelup', 'mongodown'], + options : { + path: {protocol: 'mongodb:'} + } + }, { + name : ['mssql'], + backend : 'sqldown', + dependencies: ['levelup', 'sqldown', 'knex', 'mssql'] + }, { + name : ['mysql'], + backend : 'sqldown', + dependencies: ['levelup', 'sqldown', 'knex', 'mysql'] + }, { + name : ['mysql2'], + backend : 'sqldown', + dependencies: ['levelup', 'sqldown', 'knex', 'mysql2'] + }, { + name : ['sqlite', 'sqlite3'], + backend : 'sqldown', + dependencies: ['levelup', 'sqldown', 'knex', 'sqlite3'] + }, { + name : ['pg', 'postgres', 'postgresql'], + backend : 'sqldown', + dependencies: ['levelup', 'sqldown', 'knex', 'pg', 'pg-query-stream'] + } +]; + +function wrapper(descriptor) { + + // Sanity checks + if (!descriptor) return; + if (!descriptor.name) return; + if (!descriptor.backend) return; + if (!descriptor.dependencies) return; + if (!descriptor.options) descriptor.options = {}; + + // Normalize the names + if ('string' === typeof descriptor.name) { + descriptor.name = [descriptor.name]; + } + if (!Array.isArray(descriptor.name)) { + return; + } + + // Missing dependency notifier + for (let dependency of descriptor.dependencies) { + if (!hasModule(dependency)) { + descriptor.backend = function () { + throw new Error("Missing dependency: " + dependency); + }; + break; + } + } + + // Let's build the actual wrapper + switch (typeof descriptor.options.path) { + case 'string': + switch (descriptor.options.path) { + case 'omit': + descriptor.wrapper = function (parsedLocation, options, callback) { + let backend = 'function' === typeof descriptor.backend ? descriptor.backend : require(descriptor.backend); + return require('levelup')(backend(), options, callback); + }; + break; + case 'absolute': + descriptor.wrapper = function (parsedLocation, options, callback) { + let backend = 'function' === typeof descriptor.backend ? descriptor.backend : require(descriptor.backend); + return require('levelup')(backend(toAbsolutePath(parsedLocation)), options, callback); + }; + break; + } + break; + case 'object': + descriptor.wrapper = function (parsedLocation, options, callback) { + let backend = 'function' === typeof descriptor.backend ? descriptor.backend : require(descriptor.backend); + Object.assign(parsedLocation, descriptor.options.path); + return require('levelup')(backend(parsedLocation.toString()), options, callback); + }; + break; + default: + descriptor.wrapper = function (parsedLocation, options, callback) { + let backend = 'function' === typeof descriptor.backend ? descriptor.backend : require(descriptor.backend); + return require('levelup')(backend(parsedLocation.href || parsedLocation), options, callback); + }; + break; + } + + return descriptor; +} + +module.exports = function (autolevel) { + for (let adapter of adapters) { + wrapper(adapter, autolevel); + if (!adapter.wrapper) continue; + for (let name of adapter.name) { + autolevel[name] = adapter.wrapper; + } + } +}; diff --git a/src/has-module.js b/src/has-module.js @@ -0,0 +1,15 @@ +module.exports = function hasModule( name ) { + if ( require.resolve ) { + try { + return !!require.resolve(name); + } catch(e) { + return false; + } + } + try { + require(name); + return true; + } catch(e) { + return false; + } +}; diff --git a/src/has-module.test.js b/src/has-module.test.js @@ -0,0 +1,41 @@ +import expect from 'expect'; + +// Setup environment +if ( 'object' !== typeof process ) process = {}; +if (!process.env) process.env = {}; +process.env.TEST = true; + +// Load extends +expect.extend(require('jest-isa')); + +// Load our module +let hasModule = require('./has-module'); + +test('Ensure hasModule is a function', async () => { + expect(hasModule).toBeDefined(); // Basics + expect(hasModule).isA(Function); +}); + +test('Verify some packages', async () => { + expect(hasModule('./has-module')).toBe(true); + expect(hasModule('./index')).toBe(true); + expect(hasModule('jest-isa')).toBe(true); + expect(hasModule('non-existent-module')).toBe(false); + expect(hasModule('pizza-courier')).toBe(false); +}); + +// TODO: fix this one +// test('Verify some packages without require.resolve', async () => { +// +// // Reload module +// process.env.NO_RESOLVE = true; +// console.log(process.env); +// delete require.cache[require.resolve('./has-module')]; +// hasModule = require('./has-module'); +// +// expect(hasModule('./has-module')).toBe(true); +// expect(hasModule('./index')).toBe(true); +// expect(hasModule('jest-isa')).toBe(true); +// expect(hasModule('non-existent-module')).toBe(false); +// expect(hasModule('pizza-courier')).toBe(false); +// }); diff --git a/src/index.js b/src/index.js @@ -1,22 +1,5 @@ let parseUrl = require('url-parse'); -// Check if a module is available -function hasModule( name ) { - if ( require.resolve ) { - try { - return !!require.resolve(name); - } catch(e) { - return false; - } - } - try { - require(name); - return true; - } catch(e) { - return false; - } -} - /** * Initializes a new levelup instance, based on the given options * @@ -40,11 +23,8 @@ module.exports = function autolevel( location, options, callback ) { ) || 'mem://'; } - // Load protocol initializers - require('./mem' )(hasModule, autolevel); - require('./mongodb')(hasModule, autolevel); - require('./plain' )(hasModule, autolevel); - require('./sql' )(hasModule, autolevel); + // Let the adapters register themselves + require('./adapter')(autolevel); // Parse the given location, it should contain a protocol let parsedLocation = parseUrl(location), diff --git a/src/index.test.js b/src/index.test.js @@ -1,9 +1,12 @@ import expect from 'expect'; +import rimraf from 'rimraf'; +import absolutePath from './to-absolute-path'; // Setup environment -if ( 'object' !== typeof process ) process = {}; +if ('object' !== typeof process) process = {}; if (!process.env) process.env = {}; process.env.TEST = true; +let noop = ()=>{}; // Load extends expect.extend(require('jest-isa')); @@ -33,50 +36,30 @@ test('Ensure autolevel loads', async () => { expect(instance).isA(Object); }); -// test('kv.genId', async () => { -// const known = []; -// for (let i = 0; i < 1e3; i++) { -// let generated = await kv.genId(known); -// expect(generated).isA(String); -// expect(known.indexOf(generated)).toBe(known.lastIndexOf(generated)); -// } -// for (let i = 0; i < 1e3; i++) { -// let generated = await kv.genId(known,()=>''+Math.round(Math.random())); -// expect(generated).isA(String); -// expect(known.indexOf(generated)).toBe(known.lastIndexOf(generated)); -// } -// for( let entry of testData ) { -// let generated = await kv.genId(known); -// testId.set( entry, generated ); -// expect(generated).isA(String); -// expect(known.indexOf(generated)).toBe(known.lastIndexOf(generated)); -// } -// }); -// -// test('kv.put', async () => { -// for ( let entry of testData ) { -// expect(await kv.put( testId.get(entry), entry )).toBeUndefined(); -// } -// }); -// -// test('kv.get', async () => { -// for ( let expected of testData ) { -// expect(await kv.get( testId.get(expected), opts )).toBe(expected); -// } -// }); -// -// test('kv.find', async () => { -// let result; -// -// // Search for foobar -// result = await kv.find({foo:'bar'}, opts); -// expect(result.length).toBe(1); -// expect(result[0].key).toBe( testId.get(testData[0]) ); -// expect(result[0].value).toBe( testData[0] ); -// -// // Search for 30cm BBQ Meat Lovers -// result = await kv.find({size:'>25'}, opts); -// expect(result.length).toBe(1); -// expect(result[0].key).toBe( testId.get(testData[3]) ); -// expect(result[0].value).toBe( testData[3] ); -// }); +test('Verify memory adapter', async () => { + expect(autolevel).toBeDefined(); // Basics + expect(autolevel).isA(Function); + let instance = autolevel('mem://'); + expect(await instance.put('key', 'value')).toBeUndefined(); + expect(await instance.get('key')).isA(Buffer); + expect(await instance.get('key', {asBuffer: false})).toBe('value'); +}); + +test('Verify plain adapter', async () => { + expect(autolevel).toBeDefined(); // Basics + expect(autolevel).isA(Function); + let instance = autolevel('dir://data/'); + expect(await instance.put('key', 'value')).toBeUndefined(); + expect(await instance.get('key')).isA(Buffer); + expect(await instance.get('key', {asBuffer: false})).toBe('value'); + expect(await instance.close()).toBeUndefined(); + rimraf(absolutePath('dir://data'), noop); +}); + +/* TODO: + * mssql + * mysql + * mysql2 + * mongodb + * sqlite + */ diff --git a/src/mem.js b/src/mem.js @@ -1,21 +0,0 @@ -module.exports = function( hasModule, supported ) { - - // No levelup = can't use this - if (!hasModule('levelup')) { - return; - } - - // No memdown = can't use this - if (!hasModule('memdown')) { - return; - } - - // Our initializer - supported.mem = function (parsedLocation, options, callback) { - return require('levelup')(require('memdown')(), options, callback); - }; - - // Aliases - supported.memory = supported.mem; - supported.ram = supported.mem; -}; diff --git a/src/mem.test.js b/src/mem.test.js @@ -1,35 +0,0 @@ -import expect from 'expect'; - -// Setup environment -if ( 'object' !== typeof process ) process = {}; -if (!process.env) process.env = {}; -process.env.TEST = true; - -// Load extends -expect.extend(require('jest-isa')); - -// Load our module -let autolevel = require('./index'), - adapter = false; - -test('Ensure autolevel loads', async () => { - expect(autolevel).toBeDefined(); // Basics - expect(autolevel).isA(Function); - let instance = autolevel(); - expect(instance).isA(Object); -}); - -test('Verify adapter loads', async () => { - adapter = autolevel('mem://'); - expect(adapter).isA(Object); -}); - -test('Storing data', async () => { - let result = await adapter.put('key','value'); - expect(result).toBeUndefined(); -}); - -test('Fetching data', async () => { - let result = await adapter.get('key', { asBuffer: false }); - expect(result).toBe('value'); -}); diff --git a/src/mongodb.js b/src/mongodb.js @@ -1,20 +0,0 @@ -module.exports = function( hasModule, supported ) { - - // No levelup = can't use this - if (!hasModule('levelup')) { - return; - } - - // No mongodown = can't use this - if (!hasModule('mongodown')) { - return; - } - - // Our initializer - supported.mongo = function (parsedLocation, options, callback) { - return require('levelup')( require('mongodown')(parsedLocation.href || parsedLocation), options, callback ); - }; - - // Aliases - supported.mongodb = supported.mongo; -}; diff --git a/src/mongodb.test.js b/src/mongodb.test.js @@ -1,39 +0,0 @@ -import expect from 'expect'; -import rimraf from 'rimraf'; - -// Setup environment -if ( 'object' !== typeof process ) process = {}; -if (!process.env) process.env = {}; -process.env.TEST = true; - -// Load extends -expect.extend(require('jest-isa')); - -// Load our module -let autolevel = require('./index'), - adapter = false; - -test('Ensure autolevel loads', async () => { - expect(autolevel).toBeDefined(); // Basics - expect(autolevel).isA(Function); - let instance = autolevel(); - expect(instance).isA(Object); -}); - -test('Verify adapter loads', async () => { - // TODO - // adapter = autolevel('dir://'); - // expect(adapter).isA(Object); -}); - -test('Storing data', async () => { - // TODO - // let result = await adapter.put('key','value'); - // expect(result).toBeUndefined(); -}); - -test('Fetching data', async () => { - // TODO - // let result = await adapter.get('key', { asBuffer: false }); - // expect(result).toBe('value'); -}); diff --git a/src/plain.js b/src/plain.js @@ -1,35 +0,0 @@ -const path = require('path'); - -module.exports = function (hasModule, supported) { - const basedir = global.approot || (''+require('app-root-path')); - - // Turn the parsed location into an absolute path - function toAbsolutePath( parsedLocation ) { - if ( 'string' === typeof parsedLocation ) { - parsedLocation = require('url-parse')(parsedLocation); - } - let targetLocation = parsedLocation.hostname + parsedLocation.pathname; - if ( targetLocation.substr(0,1) !== '/' ) { - targetLocation = basedir + '/' + targetLocation; - } - return targetLocation.replace(/\//g,path.sep); - } - - if (hasModule('level')) { - // Directly use level - supported.dir = function (parsedLocation, options, callback) { - return require('level')(toAbsolutePath(parsedLocation), options, callback); - }; - } else if (hasModule('levelup') && hasModule('leveldown')) { - // Combine levelup + leveldown - supported.dir = function (parsedLocation, options, callback) { - return require('levelup')(require('leveldown')(toAbsolutePath(parsedLocation), options, callback)); - }; - } else { - // Can't load this - return false; - } - - // Aliases - supported.leveldb = supported.dir; -}; diff --git a/src/plain.test.js b/src/plain.test.js @@ -1,39 +0,0 @@ -import expect from 'expect'; -import rimraf from 'rimraf'; - -// Setup environment -if ( 'object' !== typeof process ) process = {}; -if (!process.env) process.env = {}; -process.env.TEST = true; - -// Load extends -expect.extend(require('jest-isa')); - -// Load our module -let autolevel = require('./index'), - adapter = false; - -test('Ensure autolevel loads', async () => { - expect(autolevel).toBeDefined(); // Basics - expect(autolevel).isA(Function); - let instance = autolevel(); - expect(instance).isA(Object); -}); - -test('Verify adapter loads', async () => { - // TODO - // adapter = autolevel('dir://'); - // expect(adapter).isA(Object); -}); - -test('Storing data', async () => { - // TODO - // let result = await adapter.put('key','value'); - // expect(result).toBeUndefined(); -}); - -test('Fetching data', async () => { - // TODO - // let result = await adapter.get('key', { asBuffer: false }); - // expect(result).toBe('value'); -}); diff --git a/src/sql.js b/src/sql.js @@ -1,27 +0,0 @@ -module.exports = function( hasModule, supported ) { - - // No levelup = can't use this - if (!hasModule('levelup')) { - return; - } - - // No memdown = can't use this - if (!hasModule('sqldown')) { - return; - } - - // Our initializer - supported.sql = function(parsedLocation, options, callback) { - return require('levelup')(require('sqldown')(parsedLocation.href), options, callback); - }; - - // Aliases - supported.mssql = supported.sql; - supported.mysql = supported.sql; - supported.mysql2 = supported.sql; - supported.sqlite = supported.sql; - supported.sqlite3 = supported.sql; - supported.postgres = supported.sql; - supported.postgresql = supported.sql; - supported.pg = supported.sql; -}; diff --git a/src/sql.test.js b/src/sql.test.js @@ -1,39 +0,0 @@ -import expect from 'expect'; -import rimraf from 'rimraf'; - -// Setup environment -if ( 'object' !== typeof process ) process = {}; -if (!process.env) process.env = {}; -process.env.TEST = true; - -// Load extends -expect.extend(require('jest-isa')); - -// Load our module -let autolevel = require('./index'), - adapter = false; - -test('Ensure autolevel loads', async () => { - expect(autolevel).toBeDefined(); // Basics - expect(autolevel).isA(Function); - let instance = autolevel(); - expect(instance).isA(Object); -}); - -test('Verify adapter loads', async () => { - // TODO - // adapter = autolevel('dir://'); - // expect(adapter).isA(Object); -}); - -test('Storing data', async () => { - // TODO - // let result = await adapter.put('key','value'); - // expect(result).toBeUndefined(); -}); - -test('Fetching data', async () => { - // TODO - // let result = await adapter.get('key', { asBuffer: false }); - // expect(result).toBe('value'); -}); diff --git a/src/to-absolute-path.js b/src/to-absolute-path.js @@ -0,0 +1,13 @@ +const basedir = global.approot || (''+require('app-root-path')), + path = require('path'); + +module.exports = function toAbsolutePath( parsedLocation ) { + if ( 'string' === typeof parsedLocation ) { + parsedLocation = require('url-parse')(parsedLocation); + } + let targetLocation = parsedLocation.hostname + parsedLocation.pathname; + if ( targetLocation.substr(0,1) !== '/' ) { + targetLocation = basedir + '/' + targetLocation; + } + return targetLocation.replace(/\//g,path.sep); +}; diff --git a/src/to-absolute-path.test.js b/src/to-absolute-path.test.js @@ -0,0 +1,34 @@ +import expect from 'expect'; + +// Setup environment +if ( 'object' !== typeof process ) process = {}; +if (!process.env) process.env = {}; +process.env.TEST = true; +const approot = '' + require('app-root-path'), + path = require('path'), + urlParse = require('url-parse'); + +// Load extends +expect.extend(require('jest-isa')); + +// Load our module +let toAbsolutePath = require('./to-absolute-path'); + +test('Ensure toAbsolutePath is a function', async () => { + expect(toAbsolutePath).toBeDefined(); // Basics + expect(toAbsolutePath).isA(Function); +}); + +test('Test some paths', async () => { + let cases = [ + ['file:///tmp', '/tmp'], + ['/tmp', '/tmp'], + ['file://tmp', approot + path.sep + 'tmp'], + ['tmp', approot + path.sep + 'tmp'], + ]; + + for ( let testCase of cases ) { + expect(toAbsolutePath(testCase[0])).toBe(testCase[1]); + expect(toAbsolutePath(urlParse(testCase[0]))).toBe(testCase[1]); + } +}); diff --git a/update.sh b/update.sh @@ -7,6 +7,7 @@ echo "Updating .gitignore" curl -s https://www.gitignore.io/api/osx,linux,windows,node > .gitignore echo ".idea/" >> .gitignore echo ".gtm/" >> .gitignore +echo "/data/" >> .gitignore #echo "Updating modules" #git submodule foreach --recursive git clean -xfd &>/dev/null