@bcc-code/feathers-arangodb
Version:
ArangoDB Service/Adapter for FeathersJS
365 lines (364 loc) • 14.8 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.DbService = exports.AUTH_TYPES = void 0;
const errors_1 = require("@feathersjs/errors");
const database_1 = require("arangojs/database");
const aql_1 = require("arangojs/aql");
const graph_1 = require("arangojs/graph");
const isEmpty_1 = __importDefault(require("lodash/isEmpty"));
const isString_1 = __importDefault(require("lodash/isString"));
const omit_1 = __importDefault(require("lodash/omit"));
const queryBuilder_1 = require("./queryBuilder");
const logger_1 = __importDefault(require("./logger"));
const transaction_1 = require("arangojs/transaction");
const error_1 = require("arangojs/error");
const retry_database_1 = require("./retry-database");
const auto_database_1 = require("./auto-database");
var AUTH_TYPES;
(function (AUTH_TYPES) {
AUTH_TYPES["BASIC_AUTH"] = "BASIC_AUTH";
AUTH_TYPES["BEARER_AUTH"] = "BEARER_AUTH";
})(AUTH_TYPES || (exports.AUTH_TYPES = AUTH_TYPES = {}));
class DbService {
constructor(options) {
this.events = [];
// Runtime checks
/* istanbul ignore next */
if (!options.collection) {
throw new Error("A collection reference or name is required");
}
/* istanbul ignore next */
if (!options.database) {
throw new Error("A database reference or name is required");
}
/* istanbul ignore next */
if (options.id && ["_rev"].indexOf(options.id) !== -1) {
throw new Error(`Database id name of ${options.id} is a reserved key`);
}
this._id = options.id || "_id";
this.events = options.events || this.events;
this._paginate = options.paginate || {};
this.options = options;
// Set the database if passed an existing DB
/* istanbul ignore next */
if (options.database instanceof Promise) {
this._databasePromise = options.database;
}
else if (options.database instanceof database_1.Database) {
this._database = options.database;
}
else if (!(0, isString_1.default)(options.database)) {
throw new Error("Database reference or name (string) is required");
}
if (options.graph instanceof Promise) {
this._graphPromise = options.graph;
}
else if (options.graph instanceof graph_1.Graph) {
this._graph = options.graph;
}
// Set the collection if it is connected
/* istanbul ignore next */
if (options.collection instanceof Promise) {
this._collectionPromise = options.collection;
}
else if (!(0, isString_1.default)(options.collection) && !!options.collection) {
this._collection = (options.collection);
}
else if (!options.collection) {
throw new Error("Collection reference or name (string) is required");
}
// Set the view if it is connected
/* istanbul ignore next */
if (options.view instanceof Promise) {
this._viewPromise = options.view;
}
else if (!(0, isString_1.default)(options.view) && !!options.view) {
this._view = (options.view);
}
}
async connect() {
const { authType, username, password, token, graph, dbConfig, } = this.options;
if (this._database === undefined && this._databasePromise) {
this._database = await this._databasePromise;
}
/* istanbul ignore next */
if (this._database === undefined) {
let db = new retry_database_1.RetryDatabase(dbConfig);
switch (authType) {
case AUTH_TYPES.BASIC_AUTH:
db.useBasicAuth(username, password);
break;
case AUTH_TYPES.BEARER_AUTH:
/* istanbul ignore next Testing will assuming working SDK */
if (token) {
await db.useBearerAuth(token || "");
}
else {
await db.login(username, password);
}
break;
}
await db.autoUseDatabase(this.options.database);
this._database = db;
}
if (!this._graph && this._graphPromise) {
this._graph = await this._graphPromise;
}
if (graph && !this._graph) {
const { properties, opts } = graph;
if (this._database instanceof auto_database_1.AutoDatabse) {
this._graph = await this._database.autoGraph(properties, opts);
}
else {
throw `Auto creation of graphs requires instance of AutoDatabase`;
}
}
/* istanbul ignore next This doens't need to be tested */
if (this._collectionPromise) {
this._collection = await this._collectionPromise;
}
if (this._collection === undefined) {
if (this._database instanceof auto_database_1.AutoDatabse) {
this._collection = await this._database.autoCollection(this.options.collection);
}
else {
throw `Auto creation of collections requires instance of AutoDatabase`;
}
}
if (this._viewPromise) {
this._view = await this._viewPromise;
}
if (this._view === undefined) {
if (this._database instanceof auto_database_1.AutoDatabse) {
this._view = await this._database.autoView(this.options.view);
}
else {
throw `Auto creation of collections requires instance of AutoDatabase`;
}
}
return {
database: this._database,
collection: this._collection,
view: this._view
};
}
get id() {
return this._id;
}
get database() {
return this._database;
}
get collection() {
return this._collection;
}
get view() {
return this._view;
}
get paginate() {
return this._paginate;
}
set paginate(option) {
this._paginate = option || this._paginate;
}
_injectPagination(params) {
params = params || {};
if ((0, isEmpty_1.default)(this._paginate) || (params && params.paginate) === false) {
return params;
}
const paginate = params.paginate || this._paginate;
params.query = params.query || {};
let limit = parseInt(params.query.$limit);
limit =
isNaN(limit) || limit === null
? paginate.default || paginate.max || 0
: limit;
limit = Math.min(limit, paginate.max || paginate.default || limit);
params.query.$limit = limit;
return params;
}
fixKeyReturn(item) {
const idObj = {};
if (typeof item == "object" && item != null) {
idObj[this._id] = item._id;
const removeKeys = [];
if (!this.options.expandData) {
removeKeys.push("_id", "_rev");
}
return Object.assign(Object.assign({}, idObj), (0, omit_1.default)(item, removeKeys));
}
return null;
}
async _returnMap(database, query, errorMessage, removeArray = true, paging = false, transaction) {
var _a;
async function getCursor() {
try {
return await database
.query(query, { fullCount: paging });
}
catch (error) {
if ((0, error_1.isArangoError)(error) &&
error.errorNum === 1202 &&
errorMessage) {
throw new errors_1.NotFound(errorMessage);
}
else {
throw error;
}
}
}
let cursor;
if ((0, transaction_1.isArangoTransaction)(transaction))
cursor = await transaction.step(getCursor);
else
cursor = await getCursor();
const unfiltered = await cursor.map((item) => item);
const result = unfiltered.filter((item) => item != null);
if ((result.length === 0 || (result.length === 1 && result[0] == null)) &&
errorMessage) {
throw new errors_1.NotFound(errorMessage);
}
if (paging) {
return {
total: (_a = cursor.extra.stats) === null || _a === void 0 ? void 0 : _a.fullCount,
data: result,
};
}
return result.length > 1 || !removeArray ? result : result[0];
}
async find(params, existingQuery) {
var _a;
let { database, collection, view } = await this.connect();
params = this._injectPagination(params);
let queryBuilder = new queryBuilder_1.QueryBuilder(params, "doc", "doc", this.options.searchFields);
if (existingQuery) {
queryBuilder = existingQuery;
}
let useView = queryBuilder._search || this.options.enforceViewForFindQueries;
const query = aql_1.aql.join([
(0, aql_1.aql) `FOR doc in ${useView ? view : collection}`,
queryBuilder.filter ? (useView ? (0, aql_1.aql) `SEARCH` : (0, aql_1.aql) `FILTER`) : (0, aql_1.aql) ``,
(_a = queryBuilder.filter) !== null && _a !== void 0 ? _a : (0, aql_1.aql) ``,
queryBuilder.sort,
queryBuilder.limit,
queryBuilder.returnFilter,
], " ");
logger_1.default.debug("Generated aql:", { query: query.query, bindVars: query.bindVars });
const result = (await this._returnMap(database, query, undefined, false, !(0, isEmpty_1.default)(this._paginate), params.transaction));
if (!(0, isEmpty_1.default)(this._paginate)) {
return {
total: result.total,
// @ts-ignore Will be defined based on previous logic
limit: params.query.$limit || 0,
// @ts-ignore Will be defined based on previous logic
skip: params.query.$skip || 0,
data: result.data,
};
}
return result;
}
async get(id, params, existingQuery) {
const { database, collection } = await this.connect();
let queryBuilder = new queryBuilder_1.QueryBuilder(params);
if (existingQuery) {
queryBuilder = existingQuery;
}
const addedfilter = queryBuilder._aqlFilterFromFeathersQuery({ _key: id }, (0, aql_1.aql) `doc`);
queryBuilder._filter = queryBuilder._joinAqlFiltersWithOperator([queryBuilder._filter, addedfilter], queryBuilder_1.LogicalOperator.And);
const query = aql_1.aql.join([
(0, aql_1.aql) `FOR doc IN ${collection}`,
queryBuilder.filter ? (0, aql_1.aql) `FILTER` : (0, aql_1.aql) ``,
queryBuilder.filter,
queryBuilder.returnFilter,
], " ");
return this._returnMap(database, query, `No record found for id '${id}'`, true, false, params.transaction);
}
async create(data, params) {
data = Array.isArray(data) ? data : [data];
const { database, collection } = await this.connect();
const queryBuilder = new queryBuilder_1.QueryBuilder(params);
const query = (0, aql_1.aql) `
FOR item IN ${data}
INSERT item IN ${collection}
let doc = NEW
${queryBuilder.returnFilter}`;
return this._returnMap(database, query, undefined, true, false, params.transaction);
}
async _replaceOrPatch(fOpt = "REPLACE", id, data, params) {
const { database, collection } = await this.connect();
const ids = Array.isArray(id) ? id : [id];
let query;
const queryBuilder = new queryBuilder_1.QueryBuilder(params, "doc", "changed");
if (ids.length > 0 && (ids[0] != null || ids[0] != undefined)) {
query = aql_1.aql.join([
(0, aql_1.aql) `FOR doc IN ${ids}`,
aql_1.aql.literal(`${fOpt}`),
(0, aql_1.aql) `doc WITH ${data} IN ${collection}`,
(0, aql_1.aql) `LET changed = NEW`,
queryBuilder.returnFilter,
], " ");
}
else {
query = aql_1.aql.join([
(0, aql_1.aql) `FOR doc IN ${collection}`,
queryBuilder.filter ? (0, aql_1.aql) `FILTER` : (0, aql_1.aql) ``,
queryBuilder.filter,
aql_1.aql.literal(`${fOpt}`),
(0, aql_1.aql) `doc WITH ${data} IN ${collection}`,
(0, aql_1.aql) `LET changed = NEW`,
queryBuilder.returnFilter,
], " ");
}
return this._returnMap(database, query, `No record found for id '${id}'`, true, false, params.transaction);
}
async update(id, data, params) {
return this._replaceOrPatch("REPLACE", id, data, params);
}
async patch(id, data, params) {
return this._replaceOrPatch("UPDATE", id, data, params);
}
async remove(id, params) {
// Eliminate null or empty clauses
const ids = Array.isArray(id) ? id : [id];
// Setup connection & verify
const { database, collection } = await this.connect();
if (!database) {
throw new Error("Database not initialized");
}
if (!collection) {
throw new Error("Collection not initialized");
}
// Build query
let query;
const queryBuilder = new queryBuilder_1.QueryBuilder(params, "doc", "removed");
if (id && (!Array.isArray(id) || (Array.isArray(id) && id.length > 0))) {
query = (0, aql_1.aql) `
FOR doc IN ${ids}
REMOVE doc IN ${collection}
LET removed = OLD
${queryBuilder.returnFilter}
`;
}
else {
query = aql_1.aql.join([
(0, aql_1.aql) `FOR doc IN ${collection}`,
queryBuilder.filter ? (0, aql_1.aql) `FILTER` : (0, aql_1.aql) ``,
queryBuilder.filter,
(0, aql_1.aql) `REMOVE doc IN ${collection}`,
(0, aql_1.aql) `LET removed = OLD`,
queryBuilder.returnFilter,
], " ");
}
return this._returnMap(database, query, undefined, true, false, params.transaction);
}
async setup(app, path) {
await this.connect();
}
}
exports.DbService = DbService;
function ArangoDbService(options) {
return new DbService(options);
}
exports.default = ArangoDbService;