This is a start to get ArangoDB as a connector for FeathersJS
You'll need a few npm packages.
yarn add arangojs
yarn add uuid
yarn add clone-deep
First, we will create a feathers-arangodb.js
file in the /src/connectors
directory.
const Proto = require('uberproto');
const errors = require('@feathersjs/errors');
const cloneDeep = require('clone-deep');
const aql = require('arangojs').aql;
const uuidV4 = require('uuid/v4');
const { sorter, select, filterQuery, _ } = require('@feathersjs/commons');
// var _feathersCommons = require('feathers-commons');
const sift = require('sift');
const _select = (...args) => {
const base = select(...args);
return function (result) {
return base(cloneDeep(result));
};
};
class Service {
constructor (options = {}) {
this.paginate = options.paginate || {};
this._id = this.id = options.idField || options.id || '_key';
this._uId = options.startId || 0;
this.store = options.store || {};
this.events = options.events || [];
this._matcher = options.matcher;
this._sorter = options.sorter || sorter;
this._colName = options.collection;
this._db = options.db;
}
extend (obj) {
return Proto.extend(obj, this);
}
init (db = this._db, name = this._colName) {
this.db = db;
this._colName = name;
this._collection = db.collection(name);
return new Promise((resolve, reject) => {
this._collection.get()
.then(() => {
resolve(db);
})
.catch(err => {
if (err.errorNum === 1203 || err.message === 'collection not found') {
this._collection.create()
.then(() => {
resolve(db);
})
.catch(errCol => {
reject(errCol);
});
} else {
reject(err);
}
});
});
}
// Find without hooks and mixins that can be used internally and always returns
// a pagination object
_find (params, getFilter = filterQuery) {
const { query, filters } = getFilter(params.query || {});
const map = _select(params);
let values = _.values(this.store); // Won't be used....
// In-memory query
if (this._matcher) {
values = values.filter(this._matcher(query));
} else {
values = sift(query, values);
}
// Count of in-memory query result.
const total = values.length;
// Sort order
if (filters.$sort) {
values.sort(this._sorter(filters.$sort));
}
// Skip the results by slicing array
if (filters.$skip) {
values = values.slice(filters.$skip);
}
// limits by slicing off the end of the array
if (typeof filters.$limit !== 'undefined') {
values = values.slice(0, filters.$limit);
}
// Returning everything back...
return Promise.resolve({
total,
limit: filters.$limit,
skip: filters.$skip || 0,
data: map(values)
});
}
find (params) {
const paginate = typeof params.paginate !== 'undefined' ? params.paginate : this.paginate;
// Call the internal find with query parameter that include pagination
const result = this._find(params, query => filterQuery(query, paginate));
if (!(paginate && paginate.default)) {
return result.then(page => page.data);
}
return result;
}
get (id, params) {
const identifier = this._colName + '/' + id;
return this.db.query(aql`
RETURN DOCUMENT(${identifier})
`).then(cursor => cursor.next().then(doc => select(params, this.id)(cloneDeep(doc))));
}
// Create without hooks and mixins that can be used internally
_create (data, params) {
let id = data[this._id] || uuidV4();
let current = _.extend({}, _.omit(data, '_id', '_rev'), { [this._id]: id });
return this.db.query(aql`
INSERT ${current}
IN ${this._collection}
RETURN NEW
`).then(cursor => cursor.next().then(doc => select(params, this.id)(cloneDeep(doc))));
}
create (data, params) {
if (Array.isArray(data)) {
return Promise.all(data.map(current => this._create(current)));
}
return this._create(data, params);
}
// Update without hooks and mixins that can be used internally
_update (id, data, params) {
const identifier = this._colName + '/' + id;
const current = _.omit(data, this._id, '_id', '_key', '_rev');
return this.db.query(aql`
LET doc = DOCUMENT(${identifier})
REPLACE doc WITH ${current}
IN ${this._collection}
RETURN NEW
`)
.then(cursor => cursor.next().then(doc => select(params, this.id)(cloneDeep(doc))))
.catch(err => {
if (err.errorNum === 1226 || err.errorNum === 1227) return Promise.reject(new errors.NotFound(`No record found for id '${id}'`))
return Promise.reject(err);
});
}
update (id, data, params) {
if (id === null || Array.isArray(data)) {
return Promise.reject(new errors.BadRequest(
'You can not replace multiple instances. Did you mean \'patch\'?'
));
}
return this._update(id, data, params);
}
// Patch without hooks and mixins that can be used internally
_patch (id, data, params) {
const identifier = this._colName + '/' + id;
const current = _.omit(data, this._id, '_id', '_key', '_rev');
return this.db.query(aql`
LET doc = DOCUMENT(${identifier})
UPDATE doc WITH ${current}
IN ${this._collection}
RETURN NEW
`)
.then(cursor => cursor.next().then(doc => select(params, this.id)(cloneDeep(doc))))
.catch(err => {
if (err.errorNum === 1226 || err.errorNum === 1227) return Promise.reject(new errors.NotFound(`No record found for id '${id}'`))
return Promise.reject(err);
});
}
patch (id, data, params) {
if (id === null) {
return this._find(params).then(page => {
return Promise.all(page.data.map(
current => this._patch(current[this._id], data, params))
);
});
}
return this._patch(id, data, params);
}
// Remove without hooks and mixins that can be used internally
_remove (id, params) {
const identifier = this._colName + '/' + id;
return this.db.query(aql`
LET doc = DOCUMENT(${identifier})
REMOVE doc
IN ${this._collection}
RETURN OLD
`)
.then(cursor => cursor.next().then(doc => select(params, this.id)(cloneDeep(doc))))
.catch(err => {
if (err.errorNum === 1226 || err.errorNum === 1227) return Promise.reject(new errors.NotFound(`No record found for id '${id}'`))
return Promise.reject(err);
});
}
remove (id, params) {
if (id === null) {
return this._find(params).then(page =>
Promise.all(page.data.map(current =>
this._remove(current[this._id], params
)
)));
}
return this._remove(id, params);
}
}
module.exports = function init (options) {
return new Service(options);
};
module.exports.Service = Service;
Please note that the _find method is currently a copy of the memory connector. This will NOT work as is.
This is assuming you use the feathers-cli to set up a default project.
Alternatively, you could modify the next file to pull in your settings or hard-code them.
Following the CLI pattern, add this to your default.json
file in the /config
directory:
"arangodb": {
"host": "http://localhost",
"port": "8529",
"database": "feathers",
"basicAuth": {
"username": "yourUserName",
"password": "yourPassword"
}
},
Now create a arangodb.js
file in the /src
directory.
const Database = require('arangojs').Database;
module.exports = function () {
const app = this;
const config = app.get('arangodb');
const promise = dbConnect(config);
app.set('arangoClient', promise);
};
function dbConnect (options = {}) {
const host = options.host || process.env.ARANGODB_HOST;
const port = options.port || process.env.ARANGODB_PORT;
const databaseName = options.database || process.env.ARANGODB_DB;
const basicAuth = options.basicAuth || {};
const username = basicAuth.username || process.env.ARANGODB_USERNAME;
const password = basicAuth.password || process.env.ARANGODB_PASSWORD;
const url = `${host}:${port}`;
const db = new Database(url);
db.useDatabase(databaseName);
if (username) db.useBasicAuth(username, password);
return new Promise((resolve, reject) => {
db.get()
.then(() => resolve(db))
.catch(err => reject(err));
});
}
You will need to add these lines into app.js in the appropriate spots:
const arangodb = require('./arangodb');
app.configure(arangodb);
Now you can create a service using the arango connector
const createService = require('../../connectors/feathers-arangodb');
const hooks = require('./someservice.hooks');
module.exports = function (app) {
const paginate = app.get('paginate');
const arangoClient = app.get('arangoClient');
const options = {
paginate
};
// Initialize our service with any options it requires
app.use('/someservice', createService(options));
// Get our initialized service so that we can register hooks and filters
const service = app.service('someservice');
arangoClient.then(db => {
service.init(db, 'someservice');
});
service.hooks(hooks);
};