├── .babelrc ├── .editorconfig ├── .github ├── contributing.md ├── issue_template.md └── pull_request_template.md ├── .gitignore ├── .istanbul.yml ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── mocha.opts ├── package.json ├── src ├── index.js └── utils.js └── test ├── index.test.js ├── mocha.opts └── test-app.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ "add-module-exports" ], 3 | "presets": [ "es2015" ] 4 | } 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.github/contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing to Feathers 2 | 3 | Thank you for contributing to Feathers! :heart: :tada: 4 | 5 | This repo is the main core and where most issues are reported. Feathers embraces modularity and is broken up across many repos. To make this easier to manage we currently use [Zenhub](https://www.zenhub.com/) for issue triage and visibility. They have a free browser plugin you can install so that you can see what is in flight at any time, but of course you also always see current issues in Github. 6 | 7 | ## Report a bug 8 | 9 | Before creating an issue please make sure you have checked out the docs, specifically the [FAQ](https://docs.feathersjs.com/help/faq.html) section. You might want to also try searching Github. It's pretty likely someone has already asked a similar question. 10 | 11 | If you haven't found your answer please feel free to join our [slack channel](http://slack.feathersjs.com), create an issue on Github, or post on [Stackoverflow](http://stackoverflow.com) using the `feathers` or `feathersjs` tag. We try our best to monitor Stackoverflow but you're likely to get more immediate responses in Slack and Github. 12 | 13 | Issues can be reported in the [issue tracker](https://github.com/feathersjs/feathers/issues). Since feathers combines many modules it can be hard for us to assess the root cause without knowing which modules are being used and what your configuration looks like, so **it helps us immensely if you can link to a simple example that reproduces your issue**. 14 | 15 | ## Report a Security Concern 16 | 17 | We take security very seriously at Feathers. We welcome any peer review of our 100% open source code to ensure nobody's Feathers app is ever compromised or hacked. As a web application developer you are responsible for any security breaches. We do our very best to make sure Feathers is as secure as possible by default. 18 | 19 | In order to give the community time to respond and upgrade we strongly urge you report all security issues to us. Send one of the core team members a PM in [Slack](http://slack.feathersjs.com) or email us at hello@feathersjs.com with details and we will respond ASAP. 20 | 21 | For full details refer to our [Security docs](https://docs.feathersjs.com/SECURITY.html). 22 | 23 | ## Pull Requests 24 | 25 | We :heart: pull requests and we're continually working to make it as easy as possible for people to contribute, including a [Plugin Generator](https://github.com/feathersjs/generator-feathers-plugin) and a [common test suite](https://github.com/feathersjs/feathers-service-tests) for database adapters. 26 | 27 | We prefer small pull requests with minimal code changes. The smaller they are the easier they are to review and merge. A core team member will pick up your PR and review it as soon as they can. They may ask for changes or reject your pull request. This is not a reflection of you as an engineer or a person. Please accept feedback graciously as we will also try to be sensitive when providing it. 28 | 29 | Although we generally accept many PRs they can be rejected for many reasons. We will be as transparent as possible but it may simply be that you do not have the same context or information regarding the roadmap that the core team members have. We value the time you take to put together any contributions so we pledge to always be respectful of that time and will try to be as open as possible so that you don't waste it. :smile: 30 | 31 | **All PRs (except documentation) should be accompanied with tests and pass the linting rules.** 32 | 33 | ### Code style 34 | 35 | Before running the tests from the `test/` folder `npm test` will run ESlint. You can check your code changes individually by running `npm run lint`. 36 | 37 | ### ES6 compilation 38 | 39 | Feathers uses [Babel](https://babeljs.io/) to leverage the latest developments of the JavaScript language. All code and samples are currently written in ES2015. To transpile the code in this repository run 40 | 41 | > npm run compile 42 | 43 | __Note:__ `npm test` will run the compilation automatically before the tests. 44 | 45 | ### Tests 46 | 47 | [Mocha](http://mochajs.org/) tests are located in the `test/` folder and can be run using the `npm run mocha` or `npm test` (with ESLint and code coverage) command. 48 | 49 | ### Documentation 50 | 51 | Feathers documentation is contained in Markdown files in the [feathers-docs](https://github.com/feathersjs/feathers-docs) repository. To change the documentation submit a pull request to that repo, referencing any other PR if applicable, and the docs will be updated with the next release. 52 | 53 | ## External Modules 54 | 55 | If you're written something awesome for Feathers, the Feathers ecosystem, or using Feathers please add it to the [showcase](https://docs.feathersjs.com/why/showcase.html). You also might want to check out the [Plugin Generator](https://github.com/feathersjs/generator-feathers-plugin) that can be used to scaffold plugins to be Feathers compliant from the start. 56 | 57 | If you think it would be a good core module then please contact one of the Feathers core team members in [Slack](http://slack.feathersjs.com) and we can discuss whether it belongs in core and how to get it there. :beers: 58 | 59 | ## Contributor Code of Conduct 60 | 61 | As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. 62 | 63 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion. 64 | 65 | Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. 66 | 67 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team. 68 | 69 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. 70 | 71 | This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/) 72 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | ### Steps to reproduce 2 | 3 | (First please check that this issue is not already solved as [described 4 | here](https://github.com/feathersjs/feathers/blob/master/.github/contributing.md#report-a-bug)) 5 | 6 | - [ ] Tell us what broke. The more detailed the better. 7 | - [ ] If you can, please create a simple example that reproduces the issue and link to a gist, jsbin, repo, etc. 8 | 9 | ### Expected behavior 10 | Tell us what should happen 11 | 12 | ### Actual behavior 13 | Tell us what happens instead 14 | 15 | ### System configuration 16 | 17 | Tell us about the applicable parts of your setup. 18 | 19 | **Module versions** (especially the part that's not working): 20 | 21 | **NodeJS version**: 22 | 23 | **Operating System**: 24 | 25 | **Browser Version**: 26 | 27 | **React Native Version**: 28 | 29 | **Module Loader**: -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### Summary 2 | 3 | (If you have not already please refer to the contributing guideline as [described 4 | here](https://github.com/feathersjs/feathers/blob/master/.github/contributing.md#pull-requests)) 5 | 6 | - [ ] Tell us about the problem your pull request is solving. 7 | - [ ] Are there any open issues that are related to this? 8 | - [ ] Is this PR dependent on PRs in other repos? 9 | 10 | If so, please mention them to keep the conversations linked together. 11 | 12 | ### Other Information 13 | 14 | If there's anything else that's important and relevant to your pull 15 | request, mention that information here. This could include 16 | benchmarks, or other information. 17 | 18 | Your PR will be reviewed by a core team member and they will work with you to get your changes merged in a timely manner. If merged your PR will automatically be added to the changelog in the next release. 19 | 20 | If your changes involve documentation updates please mention that and link the appropriate PR in [feathers-docs](https://github.com/feathersjs/feathers-docs). 21 | 22 | Thanks for contributing to Feathers! :heart: -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | 30 | # The compiled/babelified modules 31 | lib/ 32 | -------------------------------------------------------------------------------- /.istanbul.yml: -------------------------------------------------------------------------------- 1 | verbose: false 2 | instrumentation: 3 | root: src/ 4 | excludes: 5 | - lib/ 6 | include-all-sources: true 7 | reporting: 8 | print: summary 9 | reports: 10 | - html 11 | - text 12 | - lcov 13 | watermarks: 14 | statements: [50, 80] 15 | lines: [50, 80] 16 | functions: [50, 80] 17 | branches: [50, 80] -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .editorconfig 2 | .travis.yml 3 | .babelrc 4 | .idea/ 5 | src/ 6 | test/ 7 | !lib/ 8 | .github/ 9 | coverage 10 | .istanbul.yml -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - node 4 | - '6' 5 | - '4' 6 | sudo: false 7 | before_script: 8 | - 'npm install -g codeclimate-test-reporter' 9 | after_script: 10 | - 'codeclimate-test-reporter < coverage/lcov.info' 11 | addons: 12 | code_climate: 13 | repo_token: 9fb2f356bdeef3fe301c71d1ed43ed133f569e042bd09092e1e61ce50ede101b 14 | notifications: 15 | email: false 16 | slack: 17 | rooms: 18 | secure: VPV4ReLqpOHDQFzTWx81XZpQxXdSPjbraQnc/qUY0dZQzdtXMcJGIoZfX0MBd/XLFhxb3didBt11wUBt0BfYK/sH/OddcD5z2zFkB1Z66L3Ht4FT9FVCuHNhz+4JBbv1A7M2asqkZABIYuheUFcYQ6hkOSAgBCg0asPDmsTYnzDPWy0Pr/eiYpNYh0lKM3LmslMc1YK7Dz8dsVBzsRdFq1RSuIdjL1GyXzxUwfUS4SpymZVciFSGxY6yLt4tPqMlNqN5WkwatwGYspAIPgsfEZcarNEU/bbrOMnKVjJHAPGWAi/ioWEZAJz+UOfTLtTiFddbCzliRK5T3lz/rqNdO6mWUTkoZcAKR+y4ZKMukvzk1Bi0M8Iqey0/vo8aQ8sM0/bWHHHtgaILolkGjgXdzV+FwwwUlZcC+T3KladrPwcSJ5jzzAGXHxJJhwmBhtJWNWf03jUUs9DP+zhYysNnez6apoG4lINjTiiX+gS0/Xo1WmPAHv2mQraa8BmpJBVUbbB+R7yuxGNpeZbdeWX50CKt+MFkDagGg+Mk2MQuMdZTLkZbLlE7ZgH10CkP9BXV9hKoT4rwoBSoF9R3EfZ4CMVGZQ43fWHo4IzW61f+QYDWX9dK3tS8mHw4fwjlQe7El/0f/FABBf0JUCImMIL6GSdIfySQ+TWHoB5lH+sC/KA= 19 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [v1.1.1](https://github.com/feathersjs/feathers-levelup/tree/v1.1.1) (2016-07-21) 4 | [Full Changelog](https://github.com/feathersjs/feathers-levelup/compare/v1.1.0...v1.1.1) 5 | 6 | **Merged pull requests:** 7 | 8 | - Update feathers-query-filters to version 2.0.0 🚀 [\#10](https://github.com/feathersjs/feathers-levelup/pull/10) ([greenkeeperio-bot](https://github.com/greenkeeperio-bot)) 9 | 10 | ## [v1.1.0](https://github.com/feathersjs/feathers-levelup/tree/v1.1.0) (2016-06-18) 11 | [Full Changelog](https://github.com/feathersjs/feathers-levelup/compare/v1.0.1...v1.1.0) 12 | 13 | **Closed issues:** 14 | 15 | - Support $search [\#7](https://github.com/feathersjs/feathers-levelup/issues/7) 16 | 17 | **Merged pull requests:** 18 | 19 | - Update all dependencies 🌴 [\#8](https://github.com/feathersjs/feathers-levelup/pull/8) ([greenkeeperio-bot](https://github.com/greenkeeperio-bot)) 20 | 21 | ## [v1.0.1](https://github.com/feathersjs/feathers-levelup/tree/v1.0.1) (2016-03-25) 22 | **Implemented enhancements:** 23 | 24 | - Optimized $gt, $gte, $lt, $lte during sorted range queries [\#6](https://github.com/feathersjs/feathers-levelup/issues/6) 25 | - Allow optimized reverse sorts [\#5](https://github.com/feathersjs/feathers-levelup/issues/5) 26 | - Optimize sorting for range queries [\#4](https://github.com/feathersjs/feathers-levelup/issues/4) 27 | - Write the README [\#3](https://github.com/feathersjs/feathers-levelup/issues/3) 28 | - Allow configuration of key prefix for sorting [\#2](https://github.com/feathersjs/feathers-levelup/issues/2) 29 | - Optimize range queries [\#1](https://github.com/feathersjs/feathers-levelup/issues/1) 30 | 31 | 32 | 33 | \* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)* -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Feathers 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # feathers-levelup 2 | 3 | > __Important:__ This project is no longer maintained. 4 | 5 | [![Greenkeeper badge](https://badges.greenkeeper.io/feathersjs-ecosystem/feathers-levelup.svg)](https://greenkeeper.io/) 6 | [![Maintenance](https://img.shields.io/maintenance/no/2017.svg)](#status) 7 | 8 | > A service adapter for [LevelUP](https://github.com/Level/levelup), an interface to [LevelDB](http://leveldb.org/). 9 | 10 | [![Build Status](https://travis-ci.org/feathersjs-ecosystem/feathers-levelup.png?branch=master)](https://travis-ci.org/feathersjs-ecosystem/feathers-levelup) 11 | [![Code Climate](https://codeclimate.com/github/feathersjs-ecosystem/feathers-levelup.png)](https://codeclimate.com/github/feathersjs-ecosystem/feathers-levelup) 12 | [![Test Coverage](https://codeclimate.com/github/feathersjs-ecosystem/feathers-levelup/badges/coverage.svg)](https://codeclimate.com/github/feathersjs-ecosystem/feathers-levelup/coverage) 13 | [![Dependency Status](https://img.shields.io/david/feathersjs-ecosystem/feathers-levelup.svg?style=flat-square)](https://david-dm.org/feathersjs-ecosystem/feathers-levelup) 14 | [![Download Status](https://img.shields.io/npm/dm/feathers-levelup.svg?style=flat-square)](https://www.npmjs.com/package/feathers-levelup) 15 | [![Slack Status](http://slack.feathersjs.com/badge.svg)](http://slack.feathersjs.com) 16 | 17 | **Table of Contents** 18 | 19 | - [feathers-levelup](#feathers-levelup) 20 | - [Installation](#installation) 21 | - [Documentation](#documentation) 22 | - [Getting Started](#getting-started) 23 | - [Complete Example](#complete-example) 24 | - [Key Generation and Sort Order](#key-generation-and-sort-order) 25 | - [Efficient Range Queries](#efficient-range-queries) 26 | - [Authors](#authors) 27 | - [Changelog](#changelog) 28 | - [License](#license) 29 | 30 | ## Status 31 | 32 | This module will continue to work as is but needs a maintainer for future feature and dependency updates. Create an issue if you are interested. 33 | 34 | ## Installation 35 | 36 | ```bash 37 | npm install feathers-levelup --save 38 | ``` 39 | 40 | ## Getting Started 41 | 42 | Creating a LevelUP service: 43 | 44 | ```bash 45 | npm install levelup leveldown feathers-levelup --save 46 | ``` 47 | 48 | ```js 49 | const levelup = require('levelup'); 50 | const levelupService = require('feathers-levelup'); 51 | 52 | const db = levelup('./todos', { valueEncoding: 'json' }); 53 | 54 | app.use('/todos', levelupService({ db: db })); 55 | ``` 56 | 57 | See the [LevelUP Guide](https://github.com/Level/levelup) for more information on configuring your database, including selecting a backing store. 58 | 59 | ### Complete Example 60 | 61 | Here's a complete example of a Feathers server with a `message` levelup service. 62 | 63 | ```js 64 | const service = require('./lib'); 65 | const levelup = require('levelup'); 66 | const feathers = require('feathers'); 67 | const rest = require('feathers-rest'); 68 | const bodyParser = require('body-parser'); 69 | const socketio = require('feathers-socketio'); 70 | 71 | // Create a feathers instance. 72 | const app = feathers() 73 | // Enable Socket.io 74 | .configure(socketio()) 75 | // Enable REST services 76 | .configure(rest()) 77 | // Turn on JSON parser for REST services 78 | .use(bodyParser.json()) 79 | // Turn on URL-encoded parser for REST services 80 | .use(bodyParser.urlencoded({extended: true})); 81 | 82 | // Connect to the db, create and register a Feathers service. 83 | app.use('messages', service({ 84 | db: levelup('./messages', { valueEncoding: 'json' }), 85 | paginate: { 86 | default: 2, 87 | max: 4 88 | } 89 | })); 90 | 91 | app.listen(3030); 92 | console.log('Feathers Message levelup service running on 127.0.0.1:3030'); 93 | ``` 94 | 95 | You can run this example by using `npm start` and going to [localhost:3030/messages](http://localhost:3030/messages). You should see an empty array. That's because you don't have any messages yet but you now have full CRUD for your new message service. 96 | 97 | ## Key Generation and Sort Order 98 | 99 | By default, LevelDB stores entries lexicographically [sorted by key](http://leveldb.org/). The sorting is one of the main distinguishing features of LevelDB. 100 | 101 | When feathers-levelup services `create` records, a key is generated based on a the value of `options.sortField`, plus a uuid. `_createdAt` is set and used by default, which is a good fit for time series data. 102 | 103 | Change the `sortField` option to the field of your choice to configure key ordering: 104 | 105 | ```js 106 | app.use('todos', service({ 107 | db: db, 108 | sortField: '_createdAt' // this field value will be prepended to the db key 109 | paginate: { 110 | default: 2, 111 | max: 4 112 | } 113 | })); 114 | 115 | const todos = app.service('todos'); 116 | 117 | todos 118 | .create({task: 'Buy groceries'}) 119 | .then(console.log); 120 | ``` 121 | 122 | ```js 123 | { task: 'Buy groceries', 124 | _createdAt: 1457923734510, 125 | id: '1457923734510:0:d06afc7e-f4cf-4381-a9f9-9013a6955562' } 126 | ``` 127 | 128 | ## Efficient Range Queries 129 | 130 | Avoid memory-hungry `_find` calls that load the entire key set for processing by not specifying `$sort`, or by setting it to the same field as `options.sortField`. This way `_find` can take advantage of the natural sort order of the keys in the database to traverse the fewest rows. 131 | 132 | Use `$gt`, `$gte`, `$lt`, `$lte` and `$limit` to perform fast range queries over your data. 133 | 134 | ```js 135 | app.use('todos', service({ 136 | db: db, 137 | sortField: '_createdAt' // db keys are sorted by this field value 138 | paginate: { 139 | default: 2, 140 | max: 4 141 | } 142 | })); 143 | 144 | const todos = app.service('todos'); 145 | 146 | todos 147 | .find({ 148 | query: { 149 | _createdAt: { 150 | $gt: '1457923734510' // keys starting with this _createdAt 151 | }, 152 | $limit: 10, // load the first ten 153 | $sort: { 154 | _createdAt: 1 // sort by options.sortField (or don't pass $sort at all) 155 | } 156 | } 157 | }) 158 | ``` 159 | 160 | ## Authors 161 | 162 | - [Derek Watson](http://twg.ca) 163 | 164 | ## License 165 | 166 | Copyright (c) 2016 167 | 168 | Licensed under the [MIT license](LICENSE). 169 | -------------------------------------------------------------------------------- /mocha.opts: -------------------------------------------------------------------------------- 1 | --recursive test/ 2 | --compilers js:babel-core/register -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "feathers-levelup", 3 | "description": "LevelDB adapter for Feathers via LevelUP", 4 | "version": "1.1.1", 5 | "homepage": "https://github.com/feathersjs-ecosystem/feathers-levelup", 6 | "main": "lib/", 7 | "keywords": [ 8 | "feathers", 9 | "feathers-plugin" 10 | ], 11 | "license": "MIT", 12 | "repository": { 13 | "type": "git", 14 | "url": "git://github.com/feathersjs-ecosystem/feathers-levelup.git" 15 | }, 16 | "author": { 17 | "name": "Feathers contributors", 18 | "email": "hello@feathersjs.com", 19 | "url": "https://feathersjs.com" 20 | }, 21 | "contributors": [ 22 | { 23 | "name": "Derek Watson", 24 | "email": "derek@twg.ca" 25 | } 26 | ], 27 | "bugs": { 28 | "url": "https://github.com/feathersjs-ecosystem/feathers-levelup/issues" 29 | }, 30 | "engines": { 31 | "node": ">= 4" 32 | }, 33 | "scripts": { 34 | "prepublish": "npm run compile", 35 | "publish": "git push origin --tags && npm run changelog && git push origin", 36 | "changelog": "github_changelog_generator && git add CHANGELOG.md && git commit -am \"Updating changelog\"", 37 | "release:patch": "npm version patch && npm publish", 38 | "release:minor": "npm version minor && npm publish", 39 | "release:major": "npm version major && npm publish", 40 | "compile": "rimraf lib/ && babel -d lib/ src/", 41 | "watch": "babel --watch -d lib/ src/", 42 | "lint": "eslint-if-supported semistandard --fix", 43 | "mocha": "mocha --opts mocha.opts", 44 | "test": "npm run compile && npm run lint && npm run coverage", 45 | "coverage": "istanbul cover node_modules/mocha/bin/_mocha -- --opts mocha.opts" 46 | }, 47 | "semistandard": { 48 | "env": [ 49 | "mocha" 50 | ], 51 | "ignore": [ 52 | "/lib" 53 | ] 54 | }, 55 | "directories": { 56 | "lib": "lib" 57 | }, 58 | "dependencies": { 59 | "feathers-errors": "^2.0.1", 60 | "feathers-query-filters": "^2.0.0", 61 | "levelup": "^1.3.1", 62 | "lodash": "^4.6.1", 63 | "uberproto": "^1.2.0", 64 | "uuid": "^3.0.1" 65 | }, 66 | "devDependencies": { 67 | "babel-cli": "^6.6.5", 68 | "babel-core": "^6.7.0", 69 | "babel-plugin-add-module-exports": "^0.2.1", 70 | "babel-polyfill": "^6.7.2", 71 | "babel-preset-es2015": "^6.6.0", 72 | "body-parser": "^1.15.0", 73 | "eslint-if-supported": "^1.0.1", 74 | "feathers": "^2.0.0", 75 | "feathers-rest": "^1.2.3", 76 | "feathers-service-tests": "^0.6.1", 77 | "feathers-socketio": "^2.0.0", 78 | "istanbul": "^1.1.0-alpha.1", 79 | "memdown": "^1.2.7", 80 | "mocha": "^4.0.0", 81 | "rimraf": "^2.5.4", 82 | "semistandard": "^11.0.0" 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import uuid from 'uuid'; 2 | import Proto from 'uberproto'; 3 | import errors from 'feathers-errors'; 4 | import filter from 'feathers-query-filters'; 5 | import { 6 | sorter, 7 | matchesSpecialFilters, 8 | stripSpecialFilters, 9 | filterSpecials 10 | } from './utils'; 11 | 12 | if (!global._babelPolyfill) { require('babel-polyfill'); } 13 | 14 | const _ = { 15 | values: require('lodash/values'), 16 | isEmpty: require('lodash/isEmpty'), 17 | where: require('lodash/filter'), 18 | extend: require('lodash/extend'), 19 | omit: require('lodash/omit'), 20 | pick: require('lodash/pick'), 21 | clone: require('lodash/clone') 22 | }; 23 | 24 | var counter = -1; 25 | 26 | class Service { 27 | constructor (options = {}) { 28 | if (!options.db) { 29 | throw new Error('You must provide a LevelUP database instance'); 30 | } 31 | 32 | if (options.db.options.valueEncoding !== 'json') { 33 | throw new Error('LevelUP valueEncoding option must be set to "json"'); 34 | } 35 | 36 | this.db = options.db; 37 | this.paginate = options.paginate || {}; 38 | this._id = options.idField || 'id'; 39 | this.sortField = options.sortField || '_createdAt'; 40 | } 41 | 42 | createKey (obj) { 43 | counter += 1; 44 | let uid = uuid.v4(); 45 | let prefix = (this.sortField && obj[this.sortField]) ? obj[this.sortField].toString() : ''; 46 | 47 | return `${prefix}:${counter}:${uid}`; 48 | } 49 | 50 | extend (obj) { 51 | return Proto.extend(obj, this); 52 | } 53 | 54 | // loads entire data set and performs an in-memory 55 | // filter, sort, skip, limit and select 56 | _findInMemory (query, filters) { 57 | return new Promise((resolve, reject) => { 58 | var values = []; 59 | 60 | this.db.createReadStream() 61 | .on('data', obj => { 62 | values.push(obj.value); 63 | }) 64 | .on('end', () => { 65 | values = filterSpecials(values, query); 66 | 67 | var plainQuery = stripSpecialFilters(query); 68 | 69 | if (!_.isEmpty(plainQuery)) { 70 | values = _.where(values, plainQuery); 71 | } 72 | 73 | const total = values.length; 74 | 75 | if (filters.$sort) { 76 | values.sort(sorter(filters.$sort)); 77 | } 78 | 79 | if (filters.$skip) { 80 | values = values.slice(filters.$skip); 81 | } 82 | 83 | if (filters.$limit) { 84 | values = values.slice(0, filters.$limit); 85 | } 86 | 87 | if (filters.$select) { 88 | values = values.map(value => _.pick(value, filters.$select)); 89 | } 90 | 91 | resolve({ 92 | total, 93 | limit: filters.$limit, 94 | skip: filters.$skip || 0, 95 | data: values 96 | }); 97 | }) 98 | .on('error', err => { 99 | reject(new errors.GeneralError(`Internal error reading database: ${err}`)); 100 | }); 101 | }); 102 | } 103 | 104 | // performs an efficient range query over a sorted set, 105 | // collecting matching results until we satisfy our pagination limit 106 | _findOptimized (query, filters) { 107 | return new Promise((resolve, reject) => { 108 | let total = 0; 109 | let values = []; 110 | let options = {}; 111 | let skipped = 0; 112 | 113 | if (filters.$sort) { 114 | if (filters.$sort[this.sortField] < 0) { 115 | options.reverse = true; 116 | } 117 | } 118 | 119 | if (query && query[this.sortField]) { 120 | ['gt', 'lt', 'gte', 'lte'].forEach((op) => { 121 | if (query[this.sortField].hasOwnProperty('$' + op)) { 122 | options[op] = query[this.sortField]['$' + op]; 123 | } 124 | }); 125 | } 126 | 127 | this.db.createReadStream(options) 128 | .on('data', obj => { 129 | if (!matchesSpecialFilters(obj.value, query)) { 130 | return; 131 | } 132 | 133 | var plainQuery = stripSpecialFilters(query); 134 | 135 | if (!_.isEmpty(plainQuery)) { 136 | if (_.where([obj.value], plainQuery).length === 0) { 137 | return; 138 | } 139 | } 140 | 141 | total += 1; 142 | 143 | if (filters.$skip && skipped < filters.$skip) { 144 | skipped += 1; 145 | return; 146 | } 147 | 148 | if (filters.$limit && values.length >= filters.$limit) { 149 | return; 150 | } 151 | 152 | values.push(obj.value); 153 | }) 154 | .on('end', () => { 155 | if (filters.$select) { 156 | values = values.map(value => _.pick(value, filters.$select)); 157 | } 158 | 159 | resolve({ 160 | total, 161 | limit: filters.$limit, 162 | skip: filters.$skip || 0, 163 | data: values 164 | }); 165 | }) 166 | .on('error', err => { 167 | reject(new errors.GeneralError(`Internal error reading database: ${err}`)); 168 | }); 169 | }); 170 | } 171 | 172 | _canPerformOptimized (query, filters) { 173 | if (!filters.$sort) { 174 | return true; 175 | } 176 | 177 | if (Object.keys(filters.$sort).length === 1) { 178 | if (filters.$sort.hasOwnProperty(this.sortField)) { 179 | return true; 180 | } 181 | } 182 | 183 | return false; 184 | } 185 | 186 | // Find without hooks and mixins that can be used internally and always returns 187 | // a pagination object 188 | _find (params, getFilter = filter) { 189 | const { filters, query } = getFilter(params.query || {}); 190 | 191 | return this._canPerformOptimized(query, filters) 192 | ? this._findOptimized(query, filters) 193 | : this._findInMemory(query, filters); 194 | } 195 | 196 | find (params) { 197 | const paginate = typeof params.paginate !== 'undefined' ? params.paginate : this.paginate; 198 | // Call the internal find with query parameter that include pagination 199 | const result = this._find(params, query => filter(query, paginate)); 200 | 201 | if (!paginate.default) { 202 | return result.then(page => page.data); 203 | } 204 | 205 | return result; 206 | } 207 | 208 | get (...args) { 209 | return this._get(...args); 210 | } 211 | 212 | _get (id) { 213 | return new Promise((resolve, reject) => { 214 | this.db.get(id, function (err, data) { 215 | if (err) { 216 | if (err.notFound) { 217 | return reject(new errors.NotFound(`No record found for id '${id}'`)); 218 | } 219 | return reject(new errors.GeneralError(`Internal error fetching id '${id}' (${err})`)); 220 | } 221 | 222 | resolve(data); 223 | }); 224 | }); 225 | } 226 | 227 | // Create without hooks and mixins that can be used internally 228 | _create (data) { 229 | let current = _.extend({}, data, { 230 | _createdAt: data._createdAt || new Date().getTime() 231 | }); 232 | 233 | let id = current[this._id] = this.createKey(current); 234 | 235 | return new Promise((resolve, reject) => { 236 | this.db.put(id, current, function (err) { 237 | if (err) { 238 | return reject(new errors.GeneralError(`Internal error creating id '${id}' (${err})`)); 239 | } 240 | resolve(current); 241 | }); 242 | }); 243 | } 244 | 245 | create (data) { 246 | if (Array.isArray(data)) { 247 | return Promise.all(data.map(current => this._create(current))); 248 | } 249 | 250 | return this._create(data); 251 | } 252 | 253 | update (id, data) { 254 | if (id === null || Array.isArray(data)) { 255 | return Promise.reject(new errors.BadRequest( 256 | `You can not replace multiple instances. Did you mean 'patch'?` 257 | )); 258 | } 259 | 260 | return this 261 | ._get(id) 262 | .then(() => { 263 | return new Promise((resolve, reject) => { 264 | let current = _.extend({}, data, { [this._id]: id }); 265 | 266 | this.db.put(id, current, function (err) { 267 | if (err) { 268 | return reject(new errors.GeneralError(`Internal error updating id '${id}' (${err})`)); 269 | } 270 | resolve(current); 271 | }); 272 | }); 273 | }); 274 | } 275 | 276 | // Patch without hooks and mixins that can be used internally 277 | _patch (id, updateData) { 278 | return this 279 | ._get(id) 280 | .then((old) => { 281 | return new Promise((resolve, reject) => { 282 | var current = _.extend(old, updateData); 283 | this.db.put(id, current, function (err) { 284 | if (err) { 285 | return reject(new errors.GeneralError(`Internal error updating id '${id}' (${err})`)); 286 | } 287 | resolve(current); 288 | }); 289 | }); 290 | }); 291 | } 292 | 293 | patch (id, data, params) { 294 | if (id === null) { 295 | return this._find(params).then(page => { 296 | return Promise.all(page.data.map( 297 | current => this._patch(current[this._id], data, params)) 298 | ); 299 | }); 300 | } 301 | 302 | return this._patch(id, data, params); 303 | } 304 | 305 | // Remove without hooks and mixins that can be used internally 306 | _remove (id) { 307 | return this 308 | ._get(id) 309 | .then((data) => { 310 | return new Promise((resolve, reject) => { 311 | this.db.del(id, function (err) { 312 | if (err) { 313 | return reject(new errors.GeneralError(`Internal error removing id '${id}' (${err})`)); 314 | } 315 | resolve(data); 316 | }); 317 | }); 318 | }); 319 | } 320 | 321 | remove (id, params) { 322 | if (id === null) { 323 | return this._find(params).then(page => 324 | Promise.all(page.data.map(current => this._remove(current[this._id]) 325 | ))); 326 | } 327 | 328 | return this._remove(id); 329 | } 330 | } 331 | 332 | export default function init (options) { 333 | return new Service(options); 334 | } 335 | 336 | init.Service = Service; 337 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | const _ = { 2 | some: require('lodash/some'), 3 | isMatch: require('lodash/isMatch'), 4 | isEmpty: require('lodash/isEmpty'), 5 | each: require('lodash/each'), 6 | isObject: require('lodash/isObject'), 7 | cloneDeep: require('lodash/cloneDeep') 8 | }; 9 | 10 | export const specialFilters = { 11 | $in (key, ins) { 12 | return current => ins.indexOf(current[key]) !== -1; 13 | }, 14 | 15 | $nin (key, nins) { 16 | return current => nins.indexOf(current[key]) === -1; 17 | }, 18 | 19 | $lt (key, value) { 20 | return current => current[key] < value; 21 | }, 22 | 23 | $lte (key, value) { 24 | return current => current[key] <= value; 25 | }, 26 | 27 | $gt (key, value) { 28 | return current => current[key] > value; 29 | }, 30 | 31 | $gte (key, value) { 32 | return current => current[key] >= value; 33 | }, 34 | 35 | $ne (key, value) { 36 | return current => current[key] !== value; 37 | } 38 | }; 39 | 40 | export function matchesSpecialFilters (current, query) { 41 | var matches = true; 42 | 43 | if (query.$or) { 44 | if (!_.some(query.$or, or => _.isMatch(current, or))) { 45 | matches = false; 46 | } 47 | } 48 | 49 | _.each(query, (value, key) => { 50 | if (_.isObject(value)) { 51 | _.each(value, (target, prop) => { 52 | if (specialFilters[prop]) { 53 | if (!specialFilters[prop](key, target)(current)) { 54 | matches = false; 55 | } 56 | } 57 | }); 58 | } 59 | }); 60 | 61 | return matches; 62 | } 63 | 64 | export function filterSpecials (values, query) { 65 | return values.filter(obj => matchesSpecialFilters(obj, query)); 66 | } 67 | 68 | export function stripSpecialFilters (query) { 69 | let newQuery = _.cloneDeep(query); 70 | 71 | delete newQuery.$or; 72 | 73 | _.each(newQuery, (value, key) => { 74 | if (_.isObject(value)) { 75 | _.each(value, (target, prop) => { 76 | if (specialFilters[prop]) { 77 | delete value[prop]; 78 | } 79 | }); 80 | if (_.isEmpty(value)) { 81 | delete newQuery[key]; 82 | } 83 | } 84 | }); 85 | 86 | return newQuery; 87 | } 88 | 89 | export function sorter ($sort) { 90 | return (first, second) => { 91 | let comparator = 0; 92 | _.each($sort, (modifier, key) => { 93 | modifier = parseInt(modifier, 10); 94 | 95 | if (first[key] < second[key]) { 96 | comparator -= 1 * modifier; 97 | } 98 | 99 | if (first[key] > second[key]) { 100 | comparator += 1 * modifier; 101 | } 102 | }); 103 | return comparator; 104 | }; 105 | } 106 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import service from '../src'; 3 | import levelup from 'levelup'; 4 | import memdown from 'memdown'; 5 | import feathers from 'feathers'; 6 | import server from './test-app'; 7 | import errors from 'feathers-errors'; 8 | import { base, example } from 'feathers-service-tests'; 9 | 10 | const _ids = {}; 11 | const db = levelup('/test', { db: memdown, valueEncoding: 'json' }); 12 | const app = feathers().use('/people', service({ db: db })); 13 | const people = app.service('people'); 14 | 15 | describe('Feathers LevelUP', () => { 16 | describe('base tests', () => { 17 | beforeEach(() => { 18 | return people.create({ 19 | name: 'Doug', 20 | age: 32 21 | }).then(data => { 22 | _ids.Doug = data.id; 23 | }); 24 | }); 25 | 26 | afterEach(done => { 27 | const doneNow = () => done(); 28 | 29 | people.remove(_ids.Doug).then(doneNow, doneNow); 30 | }); 31 | 32 | it('is CommonJS compatible', () => { 33 | assert.equal(typeof require('../lib'), 'function'); 34 | }); 35 | 36 | base(people, _ids, errors); 37 | }); 38 | 39 | describe('example tests', () => { 40 | after(done => server.close(() => done())); 41 | 42 | example(); 43 | }); 44 | 45 | describe('options', () => { 46 | it('requires an instance of leveldb passed as a "db" property', () => { 47 | assert.throws(() => { 48 | service({}); 49 | }, /database instance/); 50 | }); 51 | 52 | it('requires the db instance to have valueEncoding set to "json"', () => { 53 | assert.throws(() => { 54 | service({ 55 | db: levelup('/test', { db: memdown }) 56 | }); 57 | }, /valueEncoding/); 58 | }); 59 | }); 60 | 61 | describe('_createdAt', () => { 62 | it('is automatically added to newly created objects', () => { 63 | let createTime = new Date().getTime(); 64 | return people.create({ 65 | name: 'Doug', 66 | age: 32 67 | }).then(data => { 68 | let tolerance = 100; // ms 69 | assert(createTime - data._createdAt < tolerance); 70 | return people.remove(data.id); 71 | }); 72 | }); 73 | 74 | it('can be used as a sortField', () => { 75 | assert.equal(people.sortField, '_createdAt'); 76 | 77 | return people.create({ 78 | name: 'Doug', 79 | age: 32 80 | }).then(data => { 81 | assert.equal(data._createdAt, data.id.split(':')[0]); 82 | return people.remove(data.id); 83 | }); 84 | }); 85 | }); 86 | 87 | describe('sortField', () => { 88 | it('automatically prepends the prop value to the object id', () => { 89 | people.sortField = 'name'; 90 | 91 | return people.create({ 92 | name: 'Jane', 93 | age: 29 94 | }).then(data => { 95 | assert.equal('Jane', data.id.split(':')[0]); 96 | return people.remove(data.id); 97 | }); 98 | }); 99 | 100 | it('can be configured to be any stringable property', () => { 101 | people.sortField = 'age'; 102 | 103 | return people.create({ 104 | name: 'Jane', 105 | age: 29 106 | }).then(data => { 107 | assert.equal('29', data.id.split(':')[0]); 108 | return people.remove(data.id); 109 | }); 110 | }); 111 | 112 | describe('optimized range queries', () => { 113 | beforeEach(() => people.remove(null, {})); 114 | 115 | beforeEach(() => { 116 | people.sortField = 'age'; 117 | 118 | return people.create([ 119 | { 120 | name: 'Jane', 121 | age: 20 122 | }, 123 | { 124 | name: 'Rose', 125 | age: 30 126 | }, 127 | { 128 | name: 'John', 129 | age: 40 130 | } 131 | ]); 132 | }); 133 | 134 | it('sorts by sortField', () => { 135 | let params = { 136 | query: { 137 | $sort: { age: 1 } 138 | } 139 | }; 140 | 141 | return people.find(params) 142 | .then(data => { 143 | assert.deepEqual(data.map(item => item.age), [20, 30, 40]); 144 | }); 145 | }); 146 | 147 | it('reverse sorts by sortField', () => { 148 | let params = { 149 | query: { 150 | $sort: { age: -1 } 151 | } 152 | }; 153 | 154 | return people.find(params) 155 | .then(data => { 156 | assert.deepEqual(data.map(item => item.age), [40, 30, 20]); 157 | }); 158 | }); 159 | 160 | it('$lt/$gt range queries over keys', () => { 161 | assert.equal(people.sortField, 'age'); 162 | 163 | let params = { 164 | query: { 165 | age: { 166 | $gt: 29, 167 | $lt: 31 168 | } 169 | } 170 | }; 171 | 172 | return people.find(params) 173 | .then(data => { 174 | assert.equal(data.length, 1); 175 | assert.deepEqual(data.map(item => item.age), [30]); 176 | }); 177 | }); 178 | 179 | it('$lte/$gte range queries over keys', () => { 180 | assert.equal(people.sortField, 'age'); 181 | 182 | let params = { 183 | query: { 184 | age: { 185 | $gte: 30, 186 | $lte: 41 187 | } 188 | } 189 | }; 190 | 191 | return people.find(params) 192 | .then(data => { 193 | assert.equal(data.length, 2); 194 | assert.deepEqual(data.map(item => item.age), [30, 40]); 195 | }); 196 | }); 197 | }); 198 | }); 199 | }); 200 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --compilers js:babel-core/register 2 | -------------------------------------------------------------------------------- /test/test-app.js: -------------------------------------------------------------------------------- 1 | import service from '../src'; 2 | import levelup from 'levelup'; 3 | import memdown from 'memdown'; 4 | import feathers from 'feathers'; 5 | import rest from 'feathers-rest'; 6 | import bodyParser from 'body-parser'; 7 | import socketio from 'feathers-socketio'; 8 | 9 | const db = levelup('/test-app', { db: memdown, valueEncoding: 'json' }); 10 | 11 | const app = feathers() 12 | .configure(rest()) 13 | .configure(socketio()) 14 | .use(bodyParser.json()) 15 | .use(bodyParser.urlencoded({ extended: true })); 16 | 17 | app.use('/todos', service({ 18 | paginate: { 19 | default: 2, 20 | max: 4 21 | }, 22 | db: db 23 | })); 24 | 25 | module.exports = app.listen(3030); 26 | 27 | console.log('Feathers Todo levelup service running on 127.0.0.1:3030'); 28 | --------------------------------------------------------------------------------