kuzzle-sdk
Version:
Official Javascript SDK for Kuzzle
502 lines (465 loc) • 14 kB
JavaScript
const { BaseController } = require("./Base");
/* eslint sort-keys: 0 */
// Parameter mutualization
const getId = { getter: true, required: ["_id"] };
const getIdField = { getter: true, required: ["_id", "field"] };
const getKeys = { getter: true, required: ["keys"] };
const getMember = { getter: true, required: ["_id", "member"] };
const getxScan = {
getter: true,
required: ["_id", "cursor"],
opts: ["match", "count"],
mapResults: mapScanResults,
};
const getZrange = {
getter: true,
required: ["_id", "start", "stop"],
opts: assignZrangeOptions,
mapResults: mapZrangeResults,
};
const getZrangeBy = {
getter: true,
required: ["_id", "min", "max"],
opts: assignZrangeOptions,
mapResults: mapZrangeResults,
};
const setId = { required: ["_id"] };
const setIdValue = { required: ["_id", "value"] };
// Redis commands
const commands = {
append: setIdValue,
bitcount: { getter: true, required: ["_id"], opts: ["start", "end"] },
bitop: { required: ["_id", "operation", "keys"] },
bitpos: { getter: true, required: ["_id", "bit"], opts: ["start", "end"] },
dbsize: { getter: true },
decr: setId,
decrby: setIdValue,
del: { required: ["keys"] },
exists: getKeys,
expire: { required: ["_id", "seconds"], mapResults: Boolean },
expireat: { required: ["_id", "timestamp"], mapResults: Boolean },
flushdb: { mapResults: mapNoResult },
geoadd: { required: ["_id", "points"] },
geodist: {
getter: true,
required: ["_id", "member1", "member2"],
opts: ["unit"],
mapResults: parseFloat,
},
geohash: { getter: true, required: ["_id", "members"] },
geopos: {
getter: true,
required: ["_id", "members"],
mapResults: mapGeoposResults,
},
georadius: {
getter: true,
required: ["_id", "lon", "lat", "distance", "unit"],
opts: assignGeoRadiusOptions,
mapResults: mapGeoRadiusResults,
},
georadiusbymember: {
getter: true,
required: ["_id", "member", "distance", "unit"],
opts: assignGeoRadiusOptions,
mapResults: mapGeoRadiusResults,
},
get: getId,
getbit: { getter: true, required: ["_id", "offset"] },
getrange: { getter: true, required: ["_id", "start", "end"] },
getset: setIdValue,
hdel: { required: ["_id", "fields"] },
hexists: { getter: true, required: ["_id", "field"], mapResults: Boolean },
hget: getIdField,
hgetall: { getter: true, required: ["_id"] },
hincrby: { required: ["_id", "field", "value"] },
hincrbyfloat: { required: ["_id", "field", "value"], mapResults: parseFloat },
hkeys: getId,
hlen: getId,
hmget: { getter: true, required: ["_id", "fields"] },
hmset: { required: ["_id", "entries"], mapResults: mapNoResult },
hscan: getxScan,
hset: { required: ["_id", "field", "value"], mapResults: Boolean },
hsetnx: { required: ["_id", "field", "value"], mapResults: Boolean },
hstrlen: getIdField,
hvals: getId,
incr: setId,
incrby: setIdValue,
incrbyfloat: { required: ["_id", "value"], mapResults: parseFloat },
keys: { getter: true, required: ["pattern"] },
lindex: { getter: true, required: ["_id", "idx"] },
linsert: { required: ["_id", "position", "pivot", "value"] },
llen: getId,
lpop: setId,
lpush: { required: ["_id", "values"] },
lpushx: setIdValue,
lrange: { getter: true, required: ["_id", "start", "stop"] },
lrem: { required: ["_id", "count", "value"] },
lset: { required: ["_id", "index", "value"], mapResults: mapNoResult },
ltrim: { required: ["_id", "start", "stop"], mapResults: mapNoResult },
mexecute: { required: ["actions"] },
mget: getKeys,
mset: { required: ["entries"], mapResults: mapNoResult },
msetnx: { required: ["entries"], mapResults: Boolean },
object: { getter: true, required: ["_id", "subcommand"] },
persist: { required: ["_id"], mapResults: Boolean },
pexpire: { required: ["_id", "milliseconds"], mapResults: Boolean },
pexpireat: { required: ["_id", "timestamp"], mapResults: Boolean },
pfadd: { required: ["_id", "elements"], mapResults: Boolean },
pfcount: getKeys,
pfmerge: { required: ["_id", "sources"], mapResults: mapNoResult },
ping: { getter: true },
psetex: {
required: ["_id", "value", "milliseconds"],
mapResults: mapNoResult,
},
pttl: getId,
randomkey: { getter: true },
rename: { required: ["_id", "newkey"], mapResults: mapNoResult },
renamenx: { required: ["_id", "newkey"], mapResults: Boolean },
rpop: setId,
rpoplpush: { required: ["source", "destination"] },
rpush: { required: ["_id", "values"] },
rpushx: setIdValue,
sadd: { required: ["_id", "members"] },
scan: {
getter: true,
required: ["cursor"],
opts: ["match", "count"],
mapResults: mapScanResults,
},
scard: getId,
sdiff: { getter: true, required: ["_id", "keys"] },
sdiffstore: { required: ["_id", "keys", "destination"] },
set: {
required: ["_id", "value"],
opts: ["ex", "px", "nx", "xx"],
mapResults: mapNoResult,
},
setex: { required: ["_id", "value", "seconds"], mapResults: mapNoResult },
setnx: { required: ["_id", "value"], mapResults: Boolean },
sinter: getKeys,
sinterstore: { required: ["destination", "keys"] },
sismember: { getter: true, required: ["_id", "member"], mapResults: Boolean },
smembers: getId,
smove: { required: ["_id", "destination", "member"], mapResults: Boolean },
sort: {
required: ["_id"],
opts: ["alpha", "by", "direction", "get", "limit"],
},
spop: { required: ["_id"], opts: ["count"], mapResults: mapStringToArray },
srandmember: {
getter: true,
required: ["_id"],
opts: ["count"],
mapResults: mapStringToArray,
},
srem: { required: ["_id", "members"] },
sscan: getxScan,
strlen: getId,
sunion: getKeys,
sunionstore: { required: ["destination", "keys"] },
time: { getter: true, mapResults: mapArrayStringToArrayInt },
touch: { required: ["keys"] },
ttl: getId,
type: getId,
zadd: { required: ["_id", "elements"], opts: ["nx", "xx", "ch", "incr"] },
zcard: getId,
zcount: { getter: true, required: ["_id", "min", "max"] },
zincrby: { required: ["_id", "member", "value"] },
zinterstore: { required: ["_id", "keys"], opts: ["weights", "aggregate"] },
zlexcount: { getter: true, required: ["_id", "min", "max"] },
zrange: getZrange,
zrangebylex: {
getter: true,
required: ["_id", "min", "max"],
opts: ["limit"],
},
zrevrangebylex: {
getter: true,
required: ["_id", "min", "max"],
opts: ["limit"],
},
zrangebyscore: getZrangeBy,
zrank: getMember,
zrem: { required: ["_id", "members"] },
zremrangebylex: { required: ["_id", "min", "max"] },
zremrangebyrank: { required: ["_id", "start", "stop"] },
zremrangebyscore: { required: ["_id", "min", "max"] },
zrevrange: getZrange,
zrevrangebyscore: getZrangeBy,
zrevrank: getMember,
zscan: getxScan,
zscore: { getter: true, required: ["_id", "member"], mapResults: parseFloat },
zunionstore: { required: ["_id", "keys"], opts: ["weights", "aggregate"] },
};
/**
* This is a global callback pattern, called by all asynchronous functions of the Kuzzle object.
*
* @callback responseCallback
* @param {Object} err - Error object, NULL if the query is successful
* @param {Object} [data] - The content of the query response
*/
/**
* Kuzzle's memory storage is a separate data store from the database layer.
* It is internaly based on Redis. You can access most of Redis functions (all
* lowercased), except functions falling in the following categories:
*
* - blocking functions
* - cluster commands
* - configuration commands
* - cursor functions
* - database administration commands
* - debugging functions
* - script based functions
* - transaction functions
*
* @param {object} kuzzle - Kuzzle instance to inherit from
* @constructor
*/
class MemoryStorageController extends BaseController {
constructor(kuzzle) {
super(kuzzle, "ms");
}
}
// Dynamically builds this class' prototypes using the "commands" global variable
for (const action of Object.keys(commands)) {
// eslint-disable-next-line no-loop-func
MemoryStorageController.prototype[action] = function (...args) {
const command = commands[action];
const request = {
action,
};
const options = {};
if (!command.getter) {
request.body = {};
}
for (const param of command.required || []) {
const value = args.shift();
if (value === undefined) {
throw new Error(`ms.${action}: missing parameter ${param}`);
}
assignParameter(request, command.getter, param, value);
}
if (args.length > 1) {
throw new Error(`ms.${action}: too many parameters provided`);
}
if (args.length) {
if (typeof args[0] !== "object" || Array.isArray(args[0])) {
throw new Error(
`ms.${action}: invalid optional paramater (expected an object`
);
}
Object.assign(options, args[0]);
if (Array.isArray(command.opts)) {
for (const opt of command.opts) {
if (options[opt] !== null && options[opt] !== undefined) {
assignParameter(request, command.getter, opt, options[opt]);
}
}
}
}
/*
Options function mapper does not necessarily need
options to be passed by clients.
*/
if (typeof command.opts === "function") {
command.opts(request, options);
}
return this.query(request, options).then((response) => {
if (command.mapResults) {
return command.mapResults(response.result);
}
return response.result;
});
};
}
/**
*
* @param {object} data - target data object
* @param {boolean} getter - tells if the command is a getter one
* @param {string} name - parameter name
* @param {*} value - parameter value
*/
function assignParameter(data, getter, name, value) {
if (getter || name === "_id") {
data[name] = value;
} else {
data.body[name] = value;
}
}
/**
* Assign the provided options for the georadius* redis functions
* to the request object, as expected by Kuzzle API
*
* Mutates the provided data and options objects
*
* @param {object} data
* @param {object} options
*/
function assignGeoRadiusOptions(data, options) {
const parsed = [];
Object.keys(options)
.filter(function (opt) {
return (
options[opt] &&
["withcoord", "withdist", "count", "sort"].indexOf(opt) !== -1
);
})
.forEach(function (opt) {
if (opt === "withcoord" || opt === "withdist") {
parsed.push(opt);
} else if (opt === "count" || opt === "sort") {
if (opt === "count") {
parsed.push("count");
}
parsed.push(options[opt]);
}
});
if (parsed.length > 0) {
data.options = parsed;
}
}
/**
* Force the WITHSCORES option on z*range* routes
*
* Mutates the provided data and options objects
*
* @param {object} data
* @param {object} options
*/
function assignZrangeOptions(data, options) {
data.options = ["withscores"];
if (options.limit) {
data.limit = options.limit;
}
}
/**
* Maps geopos results, from array<string[]> to array<array<number>>
*
* @param {Array.<Array.<string>>} results
* @return {Array.<Array.<Number>>}
*/
function mapGeoposResults(results) {
return results.map((coords) => coords.map(parseFloat));
}
/**
* Maps georadius results to the format specified in the SDK documentation,
* preventing different formats depending on the passed options
*
* Results can be either an array of point names, or an array
* of arrays, each one of them containing the point name,
* and additional informations depending on the passed options
* (coordinates, distances)
*
* @param {Array} results
* @return {Array.<Object>}
*/
function mapGeoRadiusResults(results) {
// Simple array of point names (no options provided)
if (!Array.isArray(results[0])) {
return results.map(function (point) {
return { name: point };
});
}
return results.map(function (point) {
// The point id is always the first item
const p = {
name: point.shift(),
};
for (const elem of point) {
if (Array.isArray(elem)) {
// withcoord result are in an array...
p.coordinates = elem.map(parseFloat);
} else {
// ... and withdist are not
p.distance = parseFloat(elem);
}
}
return p;
});
}
/**
* Map a string result to an array of strings.
* Used to uniformize polymorphic results from redis
*
* @param {Array|string} results
* @return {Array.<string>}
*/
function mapStringToArray(results) {
return Array.isArray(results) ? results : [results];
}
/**
* Map an array of strings to an array of integers
*
* @param {Array.<string>} results
* @return {Array.<Number>}
*/
function mapArrayStringToArrayInt(results) {
return results.map((x) => parseInt(x));
}
/**
* Disable results for routes like flushdb
* @return {undefined}
*/
function mapNoResult() {}
/**
* Map zrange results with WITHSCORES:
* [
* "member1",
* "score of member1",
* "member2",
* "score of member2"
* ]
*
* into the following format:
* [
* {"member": "member1", "score": <score of member1>},
* {"member": "member2", "score": <score of member2>},
* ]
*
*
* @param {Array.<string>} results
* @return {Array.<Object>}
*/
function mapZrangeResults(results) {
const mapped = [];
for (let i = 0; i < results.length; i += 2) {
mapped.push({
member: results[i],
score: parseFloat(results[i + 1]),
});
}
return mapped;
}
/**
* Map *scan calls results, from:
* [
* "<cursor>",
* [
* "value1",
* "value2",
* "..."
* ]
* ]
*
* To:
* {
* cursor: <cursor>,
* values: [
* "value1",
* "value2",
* "..."
* ]
* }
*
* @param {array.<string|array>} results
* @return {object}
*/
function mapScanResults(results) {
return {
cursor: results[0],
values: results[1],
};
}
module.exports = { MemoryStorageController };