diff --git a/doc/7/controllers/ms/mexecute/index.md b/doc/7/controllers/ms/mexecute/index.md
new file mode 100644
index 000000000..06aa9afd6
--- /dev/null
+++ b/doc/7/controllers/ms/mexecute/index.md
@@ -0,0 +1,47 @@
+---
+code: true
+type: page
+title: mexecute
+---
+
+# mexecute
+
+Allows the execution of multiple commands or 'actions' in a single step.
+
+It creates a Redis **transaction** block and **executes** it immediately, all actions will be executed sequentially and as a single atomic and isolated operation.
+
+[[_Redis documentation_]](https://redis.io/topics/transactions)
+
+::: warning
+Only already valid actions of the memoryStorage controller can be executed using **mexecute**.
+:::
+
+## Arguments
+
+```js
+mexecute(actions, [options]);
+```
+
+
+
+| Arguments | Type | Description |
+| --------- | ------------------- | ------------------------------ |
+| `actions` |
object[]| List of actions to execute | +| `options` |
object| Optional query arguments | + +### actions + +The `actions` argument is an array of objects. Each object describes an action to be executed, using the following properties: + +| Property | Type | Description | +| -------- | ----------------- | ----------- | +| `action` |
string| Action name | +| `args` |
object| Arguments | + +## Resolve + +Returns an array of error & result pairs for each executed action, in order. + +## Usage + +<<< ./snippets/mexecute.js diff --git a/doc/7/controllers/ms/mexecute/snippets/mexecute.js b/doc/7/controllers/ms/mexecute/snippets/mexecute.js new file mode 100644 index 000000000..5df892766 --- /dev/null +++ b/doc/7/controllers/ms/mexecute/snippets/mexecute.js @@ -0,0 +1,13 @@ +try { + + const actions = [ + { 'action': 'set', 'args': { '_id': 'list:a', 'body': { 'value': 1, 'ex': 100, 'nx': true } } }, + { 'action': 'get', 'args': { '_id': 'list:a' } }, + { 'action': 'del', 'args': { 'body': { 'keys': ['list:a'] } } }]; + + // Prints: "[ [ null, 'OK' ], [ null, '1' ], [ null, 1 ] ]" + console.log(await kuzzle.ms.mexecute(actions)); + +} catch (error) { + console.error(error.message); +} diff --git a/doc/7/controllers/ms/mexecute/snippets/mexecute.test.yml b/doc/7/controllers/ms/mexecute/snippets/mexecute.test.yml new file mode 100644 index 000000000..1ba5492fd --- /dev/null +++ b/doc/7/controllers/ms/mexecute/snippets/mexecute.test.yml @@ -0,0 +1,7 @@ +name: ms#mexecute +description: Executes multiple commands in a single step +hooks: + before: curl -X POST kuzzle:7512/ms/_flushdb + after: +template: default +expected: "\\[ \\[ null, 'OK' ], \\[ null, '1' ], \\[ null, 1 ] ]" diff --git a/src/controllers/MemoryStorage.js b/src/controllers/MemoryStorage.js index 73713be11..1c3d06c18 100644 --- a/src/controllers/MemoryStorage.js +++ b/src/controllers/MemoryStorage.js @@ -1,168 +1,167 @@ const BaseControler = require('./Base'); // Parameter mutualization -const - getId = {getter: true, required: ['_id']}, - getIdField = {getter: true, required: ['_id', 'field']}, - getKeys = {getter: true, required: ['keys']}, - getMember = {getter: true, required: ['_id', 'member']}, - getxScan = { +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', 'cursor'], - opts: ['match', 'count'], - mapResults: mapScanResults + required: ['_id', 'member1', 'member2'], + opts: ['unit'], + mapResults: parseFloat }, - getZrange = { + geohash: {getter: true, required: ['_id', 'members']}, + geopos: {getter: true, required: ['_id', 'members'], mapResults: mapGeoposResults}, + georadius: { getter: true, - required: ['_id', 'start', 'stop'], - opts: assignZrangeOptions, - mapResults: mapZrangeResults + required: ['_id', 'lon', 'lat', 'distance', 'unit'], + opts: assignGeoRadiusOptions, + mapResults: mapGeoRadiusResults }, - getZrangeBy = { + georadiusbymember: { getter: true, - required: ['_id', 'min', 'max'], - opts: assignZrangeOptions, - mapResults: mapZrangeResults + required: ['_id', 'member', 'distance', 'unit'], + opts: assignGeoRadiusOptions, + mapResults: mapGeoRadiusResults }, - setId = {required: ['_id']}, - 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}, - 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']} - }; + 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. @@ -201,12 +200,11 @@ class MemoryStorageController extends BaseControler { for (const action of Object.keys(commands)) { // eslint-disable-next-line no-loop-func MemoryStorageController.prototype[action] = function (...args) { - const - command = commands[action], - request = { - action - }, - options = {}; + const command = commands[action]; + const request = { + action + }; + const options = {}; if (!command.getter) { request.body = {}; diff --git a/src/protocols/routes.json b/src/protocols/routes.json index 8ea556a5e..f07f9d939 100644 --- a/src/protocols/routes.json +++ b/src/protocols/routes.json @@ -552,6 +552,10 @@ "url": "/ms/_ltrim/:_id", "verb": "POST" }, + "mexecute": { + "url": "/ms/_mexecute", + "verb": "POST" + }, "mget": { "url": "/ms/_mget", "verb": "GET" diff --git a/test/controllers/memoryStorage.test.js b/test/controllers/memoryStorage.test.js index b14d8047d..63010b794 100644 --- a/test/controllers/memoryStorage.test.js +++ b/test/controllers/memoryStorage.test.js @@ -695,6 +695,24 @@ describe('MemoryStorage Controller', function () { ); }); + it('#mexecute', function () { + const actions = [ + { 'action': 'set', 'args': { '_id': 'list:a', 'body': { 'value': 1, 'ex': 100, 'nx': true } } }, + { 'action': 'get', 'args': { '_id': 'list:a' } }, + { 'action': 'del', 'args': { 'body': { 'keys': ['list:a'] } } } + ]; + + return testReadCommand( + 'mexecute', + [actions], + {}, + {body: {actions: actions}}, + {}, + [[null, 'OK'], [null, '1'], [null, 1]], + [[null, 'OK'], [null, '1'], [null, 1]], + ); + }); + it('#mget', function () { return testReadCommand( 'mget',