mirror of
https://github.com/EchoVault/SugarDB.git
synced 2025-09-27 04:16:06 +08:00
Extend SugarDB commands using JavaScript Modules (#161)
Implemented extensibility with JavaScript modules - @kelvinmwinuka
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -46,6 +46,7 @@ services:
|
|||||||
- ./internal/volumes/config:/etc/sugardb/config
|
- ./internal/volumes/config:/etc/sugardb/config
|
||||||
- ./internal/volumes/nodes/standalone_node:/var/lib/sugardb
|
- ./internal/volumes/nodes/standalone_node:/var/lib/sugardb
|
||||||
- ./internal/volumes/modules/lua:/var/lib/sugardb/scripts/lua
|
- ./internal/volumes/modules/lua:/var/lib/sugardb/scripts/lua
|
||||||
|
- ./internal/volumes/modules/js:/var/lib/sugardb/scripts/js
|
||||||
networks:
|
networks:
|
||||||
- testnet
|
- testnet
|
||||||
|
|
||||||
@@ -90,6 +91,8 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ./internal/volumes/config:/etc/sugardb/config
|
- ./internal/volumes/config:/etc/sugardb/config
|
||||||
- ./internal/volumes/nodes/cluster_node_1:/var/lib/sugardb
|
- ./internal/volumes/nodes/cluster_node_1:/var/lib/sugardb
|
||||||
|
- ./internal/volumes/modules/lua:/var/lib/sugardb/scripts/lua
|
||||||
|
- ./internal/volumes/modules/js:/var/lib/sugardb/scripts/js
|
||||||
networks:
|
networks:
|
||||||
- testnet
|
- testnet
|
||||||
|
|
||||||
@@ -134,6 +137,8 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ./internal/volumes/config:/etc/sugardb/config
|
- ./internal/volumes/config:/etc/sugardb/config
|
||||||
- ./internal/volumes/nodes/cluster_node_2:/var/lib/sugardb
|
- ./internal/volumes/nodes/cluster_node_2:/var/lib/sugardb
|
||||||
|
- ./internal/volumes/modules/lua:/var/lib/sugardb/scripts/lua
|
||||||
|
- ./internal/volumes/modules/js:/var/lib/sugardb/scripts/js
|
||||||
networks:
|
networks:
|
||||||
- testnet
|
- testnet
|
||||||
|
|
||||||
@@ -178,6 +183,8 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ./internal/volumes/config:/etc/sugardb/config
|
- ./internal/volumes/config:/etc/sugardb/config
|
||||||
- ./internal/volumes/nodes/cluster_node_3:/var/lib/sugardb
|
- ./internal/volumes/nodes/cluster_node_3:/var/lib/sugardb
|
||||||
|
- ./internal/volumes/modules/lua:/var/lib/sugardb/scripts/lua
|
||||||
|
- ./internal/volumes/modules/js:/var/lib/sugardb/scripts/js
|
||||||
networks:
|
networks:
|
||||||
- testnet
|
- testnet
|
||||||
|
|
||||||
@@ -222,6 +229,8 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ./internal/volumes/config:/etc/sugardb/config
|
- ./internal/volumes/config:/etc/sugardb/config
|
||||||
- ./internal/volumes/nodes/cluster_node_4:/var/lib/sugardb
|
- ./internal/volumes/nodes/cluster_node_4:/var/lib/sugardb
|
||||||
|
- ./internal/volumes/modules/lua:/var/lib/sugardb/scripts/lua
|
||||||
|
- ./internal/volumes/modules/js:/var/lib/sugardb/scripts/js
|
||||||
networks:
|
networks:
|
||||||
- testnet
|
- testnet
|
||||||
|
|
||||||
@@ -266,5 +275,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ./internal/volumes/config:/etc/sugardb/config
|
- ./internal/volumes/config:/etc/sugardb/config
|
||||||
- ./internal/volumes/nodes/cluster_node_5:/var/lib/sugardb
|
- ./internal/volumes/nodes/cluster_node_5:/var/lib/sugardb
|
||||||
|
- ./internal/volumes/modules/lua:/var/lib/sugardb/scripts/lua
|
||||||
|
- ./internal/volumes/modules/js:/var/lib/sugardb/scripts/js
|
||||||
networks:
|
networks:
|
||||||
- testnet
|
- testnet
|
548
docs/docs/extension/js.mdx
Normal file
548
docs/docs/extension/js.mdx
Normal file
@@ -0,0 +1,548 @@
|
|||||||
|
---
|
||||||
|
title: JavaScript Modules
|
||||||
|
toc_min_heading_level: 2
|
||||||
|
toc_max_heading_level: 4
|
||||||
|
---
|
||||||
|
|
||||||
|
import LoadModuleDocs from "@site/src/components/load_module"
|
||||||
|
import CodeBlock from "@theme/CodeBlock"
|
||||||
|
|
||||||
|
# JavaScript Modules
|
||||||
|
|
||||||
|
SugarDB allows you to create new command modules using JavaScript.
|
||||||
|
These scripts are loaded into SugarDB at runtime and can be triggered by both embedded clients and
|
||||||
|
TCP clients just like native commands.
|
||||||
|
|
||||||
|
SugarDB uses the [Otto engine (v0.5.1)](https://github.com/robertkrimen/otto) which targets ES5.
|
||||||
|
ES6 and later features will not be avaliable so you should refrain from using them.
|
||||||
|
|
||||||
|
## Creating a JavaScript Module
|
||||||
|
|
||||||
|
A JavaScript module has the following anatomy:
|
||||||
|
|
||||||
|
```js
|
||||||
|
|
||||||
|
// The keyword to trigger the command
|
||||||
|
var command = "JS.EXAMPLE"
|
||||||
|
|
||||||
|
// The string array of categories this command belongs to.
|
||||||
|
// This array can contain both built-in categories and new custom categories.
|
||||||
|
var categories = ["generic", "write", "fast"]
|
||||||
|
|
||||||
|
// The description of the command.
|
||||||
|
var description = "(JS.EXAMPLE) Example JS command that sets various data types to keys"
|
||||||
|
|
||||||
|
// Whether the command should be synced across the RAFT cluster.
|
||||||
|
var sync = true
|
||||||
|
|
||||||
|
/**
|
||||||
|
* keyExtractionFunc is a function that extracts the keys from the command and returns them to SugarDB.keyExtractionFunc
|
||||||
|
* The returned data from this function is used in the Access Control Layer to determine if the current connection is
|
||||||
|
* authorized to execute this command. The function must return a table that specifies which keys in this command
|
||||||
|
* are read keys and which ones are write keys.
|
||||||
|
* Example return: {readKeys: ["key1", "key2"], writeKeys: ["key3", "key4", "key5"]}
|
||||||
|
*
|
||||||
|
* 1. "command" is a string array representing the command that triggered this key extraction function.
|
||||||
|
*
|
||||||
|
* 2. "args" is a string array of the modifier args that were passed when loading the module into SugarDB.
|
||||||
|
* These args are passed to the key extraction function everytime it's invoked.
|
||||||
|
*/
|
||||||
|
function keyExtractionFunc(command, args) {
|
||||||
|
if (command.length > 1) {
|
||||||
|
throw "wrong number of args, expected 0"
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
readKeys: [],
|
||||||
|
writeKeys: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* handlerFunc is the command's handler function. The function is passed some arguments that allow it to interact with
|
||||||
|
* SugarDB. The function must return a valid RESP response or throw an error.
|
||||||
|
* The handler function accepts the following args:
|
||||||
|
*
|
||||||
|
* 1. "context" is a table that contains some information about the environment this command has been executed in.
|
||||||
|
* Example: {protocol: 2, database: 0}
|
||||||
|
* This object contains the following properties:
|
||||||
|
* i) protocol - the protocol version of the client that executed the command (either 2 or 3).
|
||||||
|
* ii) database - the active database index of the client that executed the command.
|
||||||
|
*
|
||||||
|
* 2. "command" is the string array representing the command that triggered this handler function.
|
||||||
|
*
|
||||||
|
* 3. "keyExists" is a function that can be called to check if a list of keys exists in the SugarDB store database.
|
||||||
|
* This function accepts a string array of keys to check and returns a table with each key having a corresponding
|
||||||
|
* boolean value indicating whether it exists.
|
||||||
|
* Examples:
|
||||||
|
* i) Example invocation: keyExists(["key1", "key2", "key3"])
|
||||||
|
* ii) Example return: {key1: true, key2: false, key3: true}
|
||||||
|
*
|
||||||
|
* 4. "getValues" is a function that can be called to retrieve values from the SugarDB store database.
|
||||||
|
* The function accepts a string array of keys whose values we would like to fetch, and returns a table with each key
|
||||||
|
* containing the corresponding value from the store.
|
||||||
|
* The possible data types for the values are: number, string, nil, hash, set, zset
|
||||||
|
* Examples:
|
||||||
|
* i) Example invocation: getValues(["key1", "key2", "key3"])
|
||||||
|
* ii) Example return: {key1: 3.142, key2: nil, key3: "Pi"}
|
||||||
|
*
|
||||||
|
* 5. "setValues" is a function that can be called to set values in the active database in the SugarDB store.
|
||||||
|
* This function accepts a table with keys and the corresponding values to set for each key in the active database
|
||||||
|
* in the store.
|
||||||
|
* The accepted data types for the values are: number, string, nil, hash, set, zset.
|
||||||
|
* The setValues function does not return anything.
|
||||||
|
* Examples:
|
||||||
|
* i) Example invocation: setValues({key1: 3.142, key2: nil, key3: "Pi"})
|
||||||
|
*
|
||||||
|
* 6. "args" is a string array of the modifier args passed to the module at load time. These args are passed to the
|
||||||
|
* handler everytime it's invoked.
|
||||||
|
*/
|
||||||
|
function handlerFunc(ctx, command, keysExist, getValues, setValues, args) {
|
||||||
|
// Set various data types to keys
|
||||||
|
var keyValues = {
|
||||||
|
"numberKey": 42,
|
||||||
|
"stringKey": "Hello, SugarDB!",
|
||||||
|
"floatKey": 3.142,
|
||||||
|
"nilKey": null,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the values in the database
|
||||||
|
setValues(keyValues)
|
||||||
|
|
||||||
|
// Verify the values have been set correctly
|
||||||
|
var keysToGet = ["numberKey", "stringKey", "floatKey", "nilKey"]
|
||||||
|
var retrievedValues = getValues(keysToGet)
|
||||||
|
|
||||||
|
// Create an array to track mismatches
|
||||||
|
var mismatches = [];
|
||||||
|
for (var key in keyValues) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(keyValues, key)) {
|
||||||
|
var expectedValue = keyValues[key];
|
||||||
|
var retrievedValue = retrievedValues[key];
|
||||||
|
if (retrievedValue !== expectedValue) {
|
||||||
|
var msg = "Key " + key + ": expected " + expectedValue + ", got " + retrievedValue
|
||||||
|
mismatches.push(msg);
|
||||||
|
console.log(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If mismatches exist, return an error
|
||||||
|
if (mismatches.length > 0) {
|
||||||
|
throw "values mismatch"
|
||||||
|
}
|
||||||
|
|
||||||
|
// If all values match, return OK
|
||||||
|
return "+OK\r\n"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Loading JavaScript Modules
|
||||||
|
<LoadModuleDocs module="js" />
|
||||||
|
|
||||||
|
## Standard Data Types
|
||||||
|
|
||||||
|
Sugar DB supports the following standard data types in JavaScript modules:
|
||||||
|
|
||||||
|
- string
|
||||||
|
- number (integers and floating-point numbers)
|
||||||
|
- null
|
||||||
|
- arrays (tables with integer keys)
|
||||||
|
|
||||||
|
These data types can be stored using the setValues function and retrieved using the getValues function.
|
||||||
|
|
||||||
|
## Custom Data Types
|
||||||
|
|
||||||
|
In addition to the standard data types, SugarDB also supports custom data types in JavaScript modules.
|
||||||
|
These custom data types include:
|
||||||
|
|
||||||
|
- Hashes
|
||||||
|
- Sets
|
||||||
|
- Sorted Sets
|
||||||
|
|
||||||
|
Just like the standard types, these custom data types can be stored and retrieved using the setValues
|
||||||
|
and getValues functions respectively.
|
||||||
|
|
||||||
|
### Hashes
|
||||||
|
|
||||||
|
The hash data type is a custom data type in SugarDB designed for storing and managing key-value pairs.
|
||||||
|
It supports several methods for interacting with the hash, including adding, updating, retrieving, deleting,
|
||||||
|
and checking values.This section explains how to make use of the hash data type in your JavaScript modules.
|
||||||
|
|
||||||
|
#### Creating a Hash
|
||||||
|
|
||||||
|
```js
|
||||||
|
var myHash = new Hash();
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Hash methods
|
||||||
|
|
||||||
|
`set` - Adds or updates key-value pairs in the hash. If the key exists,
|
||||||
|
the value is updated; otherwise, it is added.
|
||||||
|
|
||||||
|
```js
|
||||||
|
var myHash = new Hash();
|
||||||
|
var numUpdated = myHash.set({
|
||||||
|
"field1": "value1",
|
||||||
|
"field2": "value2",
|
||||||
|
"field3": "value3",
|
||||||
|
"field4": "value4"
|
||||||
|
});
|
||||||
|
console.log(numUpdated) // Output: 4
|
||||||
|
```
|
||||||
|
|
||||||
|
`setnx` - Adds key-value pairs to the hash only if the key does not already exist.
|
||||||
|
|
||||||
|
```js
|
||||||
|
var myHash = new Hash();
|
||||||
|
myHash.set({"field1": "value1"});
|
||||||
|
var numAdded = myHash.setnx({
|
||||||
|
"field1": "newValue", // Will not overwrite because field1 exists
|
||||||
|
"field2": "value2" // Will be added
|
||||||
|
})
|
||||||
|
console.log(numAdded) // Output: 1
|
||||||
|
```
|
||||||
|
|
||||||
|
`get` - Retrieves the values for the specified keys. Returns nil for keys that do not exist.
|
||||||
|
|
||||||
|
```js
|
||||||
|
var myHash = new Hash();
|
||||||
|
myHash.set({
|
||||||
|
key1: "value1" ,
|
||||||
|
key2: "value2"
|
||||||
|
});
|
||||||
|
// Get values from the hash
|
||||||
|
var values = myHash.get(["key1", "key2", "key3"]);
|
||||||
|
// Iterate over the values and log them
|
||||||
|
for (var key in values) {
|
||||||
|
if (values.hasOwnProperty(key)) {
|
||||||
|
console.log(key, values[key]); // Output: key1 value1, key2 value2, key3 undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`len` - Returns the number of key-value pairs in the hash.
|
||||||
|
|
||||||
|
```js
|
||||||
|
var myHash = new Hash();
|
||||||
|
myHash.set({
|
||||||
|
"key1": "value1",
|
||||||
|
"key2": "value2"
|
||||||
|
});
|
||||||
|
console.log(myHash:len()) // Output: 2
|
||||||
|
```
|
||||||
|
|
||||||
|
`all` - Returns a table containing all key-value pairs in the hash.
|
||||||
|
|
||||||
|
```js
|
||||||
|
var myHash = new Hash();
|
||||||
|
myHash.set({
|
||||||
|
"key1": "value1",
|
||||||
|
"key2": "value2"
|
||||||
|
});
|
||||||
|
var allKVPairs = myHash:all()
|
||||||
|
for (var key in allKVPairs) {
|
||||||
|
if (allKVPairs.hasOwnProperty(key)) {
|
||||||
|
console.log(key, allKVPairs[key]); // Output: key1 value1, key2 value2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`exists` - Checks if specified keys exist in the hash.
|
||||||
|
|
||||||
|
```js
|
||||||
|
var myHash = new Hash();
|
||||||
|
myHash.set({
|
||||||
|
"key1": "value1"
|
||||||
|
});
|
||||||
|
var existence = myHash.exists(["key1", "key2"])
|
||||||
|
for (var key in existence) {
|
||||||
|
if (existence.hasOwnProperty(key)) {
|
||||||
|
console.log(key, existence[key]); // Output: key1 true, key2 false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`del` - Deletes the specified keys from the hash. Returns the number of keys deleted.
|
||||||
|
|
||||||
|
```js
|
||||||
|
var myHash = new Hash();
|
||||||
|
myHash.set({
|
||||||
|
"key1": "value1",
|
||||||
|
"key2": "value2"
|
||||||
|
});
|
||||||
|
var numDeleted = myHash.del(["key1", "key3"])
|
||||||
|
console.log(numDeleted) // Output: 1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sets
|
||||||
|
|
||||||
|
The `set` data type is a custom data type in SugarDB designed for managing unique elements.
|
||||||
|
It supports operations like adding, removing, checking for membership,
|
||||||
|
and performing set operations such as subtraction.
|
||||||
|
This section explains how to use the `set` data type in your JavaScript modules.
|
||||||
|
|
||||||
|
#### Creating a Set
|
||||||
|
|
||||||
|
```js
|
||||||
|
var mySet1 = new Set(); // Create new empty set
|
||||||
|
var mySet2 = new Set(["apple", "banana", "cherry"]) // Create new set with elements
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Set methods
|
||||||
|
|
||||||
|
`add` - Adds one or more elements to the set. Returns the number of elements added.
|
||||||
|
|
||||||
|
```js
|
||||||
|
var mySet = new Set();
|
||||||
|
var addedCount = mySet.add(["apple", "banana"])
|
||||||
|
console.log(addedCount) // Output: 2
|
||||||
|
```
|
||||||
|
|
||||||
|
`pop` - Removes and returns a specified number of random elements from the set.
|
||||||
|
|
||||||
|
```js
|
||||||
|
var mySet = new Set(["apple", "banana", "cherry"])
|
||||||
|
var popped = mySet.pop(2)
|
||||||
|
console.log(popped) // Outputs an array of 2 random elements from the set
|
||||||
|
```
|
||||||
|
|
||||||
|
`contains` - Checks if a specific element exists in the set.
|
||||||
|
|
||||||
|
```js
|
||||||
|
var mySet = new Set(["apple", "banana"])
|
||||||
|
console.log(mySet.contains("apple")) // Output: true
|
||||||
|
console.log(mySet.contains("cherry")) // Output: false
|
||||||
|
```
|
||||||
|
|
||||||
|
`cardinality` - Returns the number of elements in the set.
|
||||||
|
|
||||||
|
```js
|
||||||
|
var mySet = new Set(["apple", "banana"])
|
||||||
|
console.log(mySet.cardinality()) // Output: 2
|
||||||
|
```
|
||||||
|
|
||||||
|
`remove` - Removes one or more specified elements from the set. Returns the number of elements removed.
|
||||||
|
|
||||||
|
```js
|
||||||
|
var mySet = new Set(["apple", "banana", "cherry"])
|
||||||
|
var removedCount = mySet.remove(["banana", "cherry"])
|
||||||
|
console.log(removedCount) // Output: 2
|
||||||
|
```
|
||||||
|
|
||||||
|
`move` - Moves an element from one set to another. Returns true if the element was successfully moved.
|
||||||
|
|
||||||
|
```js
|
||||||
|
var set1 = new Set(["apple", "banana"])
|
||||||
|
var set2 = new Set(["cherry"])
|
||||||
|
var success = set1.move(set2, "banana")
|
||||||
|
console.log(success) // Output: true
|
||||||
|
```
|
||||||
|
|
||||||
|
`subtract` - Returns a new set that is the result of subtracting other sets from the current set.
|
||||||
|
|
||||||
|
```js
|
||||||
|
var set1 = new Set(["apple", "banana", "cherry"])
|
||||||
|
var set2 = new Set(["banana"])
|
||||||
|
var resultSet = set1.subtract([set2])
|
||||||
|
var allElems = resultSet.all()
|
||||||
|
for (var i = 0; i < allElems.length; i++) {
|
||||||
|
console.log(allElems[i]); // Output: "apple", "cherry"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`all` - Returns a table containing all elements in the set.
|
||||||
|
|
||||||
|
```js
|
||||||
|
var mySet = new Set(["apple", "banana", "cherry"])
|
||||||
|
var allElems = mySet.all()
|
||||||
|
for (var i = 0; i < allElems.length; i++) {
|
||||||
|
console.log(allElems[i]); // Output: "apple", "banana", "cherry"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`random` - Returns a table of randomly selected elements from the set. The number of elements to return is specified as an argument.
|
||||||
|
|
||||||
|
```js
|
||||||
|
var mySet = new Set(["apple", "banana", "cherry", "date"])
|
||||||
|
var randomElems = mySet.random(2)
|
||||||
|
console.log(randomElems) // Outputs an array of 2 random elements from the set
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sorted Sets
|
||||||
|
|
||||||
|
A zset is a sorted set that stores zmember elements, ordered by their score.
|
||||||
|
The zset type provides methods to manipulate and query the set. A zset is made up of
|
||||||
|
zmember elements, each of which has a value and a score.
|
||||||
|
|
||||||
|
#### zmember
|
||||||
|
|
||||||
|
A zmember represents an element in a zset (sorted set). Each zmember consists of:
|
||||||
|
- value: A unique identifier for the member (e.g., a string).
|
||||||
|
- score: A numeric value used to sort the member in the sorted set.
|
||||||
|
|
||||||
|
You can create a zmember as follows:
|
||||||
|
|
||||||
|
```js
|
||||||
|
var m = new ZMember({value: "example", score: 42})
|
||||||
|
```
|
||||||
|
|
||||||
|
The zmember type provides methods to retrieve or modify these properties.
|
||||||
|
|
||||||
|
To set/get the value of a zmember, use the `value` method:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// Get the value
|
||||||
|
var value = m.value()
|
||||||
|
|
||||||
|
// Set the value
|
||||||
|
m.value("new_value")
|
||||||
|
```
|
||||||
|
|
||||||
|
To set/get the score, use the `score` method:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// Get the score
|
||||||
|
var score = m.score()
|
||||||
|
|
||||||
|
// Set the score
|
||||||
|
m.score(99.5)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Creating a Sorted Set
|
||||||
|
|
||||||
|
```js
|
||||||
|
// Create a new zset with no zmembers
|
||||||
|
var zset1 = new ZSet()
|
||||||
|
|
||||||
|
// Create a new zset with two zmembers
|
||||||
|
var zset2 = new ZSet([
|
||||||
|
new ZMember({value: "a", score: 10}),
|
||||||
|
new ZMember({value: "b", score: 20}),
|
||||||
|
])
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Sorted Set Methods
|
||||||
|
|
||||||
|
`add` - Adds one or more zmember elements to the zset.
|
||||||
|
Optionally, you can specify update policies using the optional modifiers.
|
||||||
|
|
||||||
|
Optional Modifiers:
|
||||||
|
- "exists": Specifies whether to only update existing members ("xx") or only add new members ("nx"). Defaults to no restriction.
|
||||||
|
- "comparison": Specifies a comparison method for updating scores (e.g., "min", "max").
|
||||||
|
- "changed": If true, returns the count of changed elements.
|
||||||
|
- "incr": If true, increments the score of the specified member by the given score instead of replacing it.
|
||||||
|
|
||||||
|
Basic usage:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// Create members
|
||||||
|
var m1 = new ZMember({value: "item1", score: 10})
|
||||||
|
var m2 = new ZMember({value: "item2", score: 20})
|
||||||
|
|
||||||
|
// Create zset and add members
|
||||||
|
var zset = new ZSet()
|
||||||
|
zset.add([m1, m2])
|
||||||
|
|
||||||
|
// Check cardinality
|
||||||
|
console.log(zset.cardinality()) // Outputs: 2
|
||||||
|
```
|
||||||
|
|
||||||
|
Usage with optional modifiers:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// Create zset
|
||||||
|
var zset = new ZSet([
|
||||||
|
new ZMember({value: "a", score: 10}),
|
||||||
|
new ZMember({value: "b", score: 20}),
|
||||||
|
])
|
||||||
|
|
||||||
|
// Attempt to add members with different policies
|
||||||
|
var new_members = {
|
||||||
|
new ZMember({value: "a", score: 5}), // Existing member
|
||||||
|
new ZMember({value: "c", score: 15}), // New member
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use policies to update and add
|
||||||
|
var options = {
|
||||||
|
exists = "xx", // Only update existing members
|
||||||
|
comparison = "max", // Keep the maximum score for existing members
|
||||||
|
changed = true, // Return the count of changed elements
|
||||||
|
}
|
||||||
|
var changed_count = zset.add(new_members, options)
|
||||||
|
|
||||||
|
// Display results
|
||||||
|
console.log("Changed count:", changed_count) // Outputs: 1 (only "a" is updated)
|
||||||
|
|
||||||
|
// Adding with different policies
|
||||||
|
var incr_options = {
|
||||||
|
exists = "nx", // Only add new members
|
||||||
|
incr = true, // Increment the score of the added members
|
||||||
|
}
|
||||||
|
zset.add([new ZMember({value: "d", score: 10})], incr_options)
|
||||||
|
```
|
||||||
|
|
||||||
|
`update` - Updates one or more zmember elements in the zset.
|
||||||
|
If the member doesn’t exist, the behavior depends on the provided update options.
|
||||||
|
|
||||||
|
Optional Modifiers:
|
||||||
|
- "exists": Specifies whether to only update existing members ("xx") or only add new members ("nx"). Defaults to no restriction.
|
||||||
|
- "comparison": Specifies a comparison method for updating scores (e.g., "min", "max").
|
||||||
|
- "changed": If true, returns the count of changed elements.
|
||||||
|
- "incr": If true, increments the score of the specified member by the given score instead of replacing it.
|
||||||
|
|
||||||
|
```js
|
||||||
|
// Create members
|
||||||
|
var m1 = new ZMember({value: "item1", score: 10})
|
||||||
|
var m2 = new ZMember({value: "item2", score: 20})
|
||||||
|
|
||||||
|
// Create zset and add members
|
||||||
|
var zset = new ZSet([m1, m2])
|
||||||
|
|
||||||
|
// Update a member
|
||||||
|
var m_update = new ZMember({value: "item1", score: 15})
|
||||||
|
var changed_count = zset.update([m_update], {exists = true, comparison = "max", changed = true})
|
||||||
|
console.log("Changed count:", changed_count) // Outputs the number of elements updated
|
||||||
|
```
|
||||||
|
|
||||||
|
`remove` - Removes a member from the zset by its value.
|
||||||
|
|
||||||
|
```js
|
||||||
|
var removed = zset.remove("a") // Returns true if removed
|
||||||
|
```
|
||||||
|
|
||||||
|
`cardinality` - Returns the number of zmembers in the zset.
|
||||||
|
|
||||||
|
```js
|
||||||
|
var count = zset.cardinality()
|
||||||
|
```
|
||||||
|
|
||||||
|
`contains` - Checks if a zmember with the specified value exists in the zset.
|
||||||
|
|
||||||
|
```js
|
||||||
|
var exists = zset.contains("b") // Returns true if exists
|
||||||
|
```
|
||||||
|
|
||||||
|
`random` - Returns a random zmember from the zset.
|
||||||
|
|
||||||
|
```js
|
||||||
|
var members = zset.random(2) // Returns up to 2 random members
|
||||||
|
```
|
||||||
|
|
||||||
|
`all` - Returns all zmembers in the zset.
|
||||||
|
|
||||||
|
```js
|
||||||
|
var members = zset.all()
|
||||||
|
for (var i = 0; i < members.length; i++) {
|
||||||
|
console.log(members[i].value(), members[i].score())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`subtract` - Returns a new zset that is the result of subtracting other zsets from the current one.
|
||||||
|
|
||||||
|
```js
|
||||||
|
var other_zset = new ZSet([
|
||||||
|
new ZMember({value: "b", score: 20}),
|
||||||
|
])
|
||||||
|
var result_zset = zset.subtract([other_zset])
|
||||||
|
```
|
||||||
|
|
@@ -44,9 +44,6 @@ Example return: {["readKeys"] = {"key1", "key2"}, ["writeKeys"] = {"key3", "key4
|
|||||||
These args are passed to the key extraction function everytime it's invoked.
|
These args are passed to the key extraction function everytime it's invoked.
|
||||||
]]
|
]]
|
||||||
function keyExtractionFunc (command, args)
|
function keyExtractionFunc (command, args)
|
||||||
for k,v in pairs(args) do
|
|
||||||
print(k, v)
|
|
||||||
end
|
|
||||||
if (#command ~= 1) then
|
if (#command ~= 1) then
|
||||||
error("wrong number of args, expected 0")
|
error("wrong number of args, expected 0")
|
||||||
end
|
end
|
||||||
@@ -138,8 +135,7 @@ Sugar DB supports the following standard data types in Lua scripts:
|
|||||||
- nil
|
- nil
|
||||||
- arrays (tables with integer keys)
|
- arrays (tables with integer keys)
|
||||||
|
|
||||||
These data types can be used to stored using the setValues function and
|
These data types can be stored using the setValues function and retrieved using the getValues function.
|
||||||
retrieved using the getValues function.
|
|
||||||
|
|
||||||
## Custom Data Types
|
## Custom Data Types
|
||||||
|
|
||||||
@@ -439,13 +435,7 @@ local options = {
|
|||||||
changed = true, -- Return the count of changed elements
|
changed = true, -- Return the count of changed elements
|
||||||
}
|
}
|
||||||
local changed_count = zset:add(new_members, options)
|
local changed_count = zset:add(new_members, options)
|
||||||
|
|
||||||
-- Display results
|
|
||||||
print("Changed count:", changed_count) -- Outputs: 1 (only "a" is updated)
|
print("Changed count:", changed_count) -- Outputs: 1 (only "a" is updated)
|
||||||
print("Updated zset:")
|
|
||||||
for _, member in ipairs(zset:all()) do
|
|
||||||
print(member:value(), member:score())
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Adding with different policies
|
-- Adding with different policies
|
||||||
local incr_options = {
|
local incr_options = {
|
||||||
@@ -453,12 +443,6 @@ local incr_options = {
|
|||||||
incr = true, -- Increment the score of the added members
|
incr = true, -- Increment the score of the added members
|
||||||
}
|
}
|
||||||
zset:add({zmember.new({value = "d", score = 10})}, incr_options)
|
zset:add({zmember.new({value = "d", score = 10})}, incr_options)
|
||||||
|
|
||||||
-- Display updated zset
|
|
||||||
print("After adding with increment:")
|
|
||||||
for _, member in ipairs(zset:all()) do
|
|
||||||
print(member:value(), member:score())
|
|
||||||
end
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`update` - Updates one or more zmember elements in the zset.
|
`update` - Updates one or more zmember elements in the zset.
|
||||||
|
@@ -1,9 +1,13 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import CodeBlock from "@theme/CodeBlock";
|
import CodeBlock from "@theme/CodeBlock";
|
||||||
|
|
||||||
const LoadModuleDocs = ({ module }: { module: "lua" | "go" }) => {
|
const LoadModuleDocs = ({ module }: { module: "go" | "lua" | "js" }) => {
|
||||||
const module_path =
|
const module_path =
|
||||||
module === "lua" ? "path/to/module/module.lua" : "path/to/module/module.so";
|
module === "go"
|
||||||
|
? "path/to/module/module.so"
|
||||||
|
: module === "lua"
|
||||||
|
? "path/to/module/module.lua"
|
||||||
|
: "path/to/module/module.js";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
3
go.mod
3
go.mod
@@ -30,8 +30,11 @@ require (
|
|||||||
github.com/mattn/go-colorable v0.1.12 // indirect
|
github.com/mattn/go-colorable v0.1.12 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||||
github.com/miekg/dns v1.1.26 // indirect
|
github.com/miekg/dns v1.1.26 // indirect
|
||||||
|
github.com/robertkrimen/otto v0.5.1 // indirect
|
||||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect
|
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect
|
||||||
golang.org/x/crypto v0.14.0 // indirect
|
golang.org/x/crypto v0.14.0 // indirect
|
||||||
golang.org/x/net v0.16.0 // indirect
|
golang.org/x/net v0.16.0 // indirect
|
||||||
golang.org/x/sys v0.13.0 // indirect
|
golang.org/x/sys v0.13.0 // indirect
|
||||||
|
golang.org/x/text v0.13.0 // indirect
|
||||||
|
gopkg.in/sourcemap.v1 v1.0.5 // indirect
|
||||||
)
|
)
|
||||||
|
6
go.sum
6
go.sum
@@ -112,6 +112,8 @@ github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R
|
|||||||
github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||||
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
|
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
|
||||||
|
github.com/robertkrimen/otto v0.5.1 h1:avDI4ToRk8k1hppLdYFTuuzND41n37vPGJU7547dGf0=
|
||||||
|
github.com/robertkrimen/otto v0.5.1/go.mod h1:bS433I4Q9p+E5pZLu7r17vP6FkE6/wLxBdmKjoqJXF8=
|
||||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I=
|
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I=
|
||||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||||
github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE=
|
github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE=
|
||||||
@@ -165,6 +167,8 @@ golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
|||||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
|
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
|
||||||
|
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
@@ -174,6 +178,8 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8
|
|||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/sourcemap.v1 v1.0.5 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI=
|
||||||
|
gopkg.in/sourcemap.v1 v1.0.5/go.mod h1:2RlvNNSMglmRrcvhfuzp4hQHwOtjxlbjX7UPY/GXb78=
|
||||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
@@ -213,11 +213,6 @@ type HandlerFuncParams struct {
|
|||||||
// scriptType is either "FILE" or "RAW".
|
// scriptType is either "FILE" or "RAW".
|
||||||
// content contains the file path if scriptType is "FILE" and the raw script if scriptType is "RAW"
|
// content contains the file path if scriptType is "FILE" and the raw script if scriptType is "RAW"
|
||||||
AddScript func(engine string, scriptType string, content string, args []string) error
|
AddScript func(engine string, scriptType string, content string, args []string) error
|
||||||
// AddScriptCommand adds a commands to SugarDB that is defined by a scripting language.
|
|
||||||
// engine defines the interpreter to be used. Possible values: "LUA"
|
|
||||||
// scriptType is either "FILE" or "RAW".
|
|
||||||
// content contains the file path if scriptType is "FILE" and the raw script if scriptType is "RAW".
|
|
||||||
AddScriptCommand func(content string, args []string) error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandlerFunc is a functions described by a command where the bulk of the command handling is done.
|
// HandlerFunc is a functions described by a command where the bulk of the command handling is done.
|
||||||
|
113
internal/volumes/modules/js/example.js
Normal file
113
internal/volumes/modules/js/example.js
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
|
||||||
|
// The keyword to trigger the command
|
||||||
|
var command = "JS.EXAMPLE"
|
||||||
|
|
||||||
|
// The string array of categories this command belongs to.
|
||||||
|
// This array can contain both built-in categories and new custom categories.
|
||||||
|
var categories = ["generic", "write", "fast"]
|
||||||
|
|
||||||
|
// The description of the command.
|
||||||
|
var description = "(JS.EXAMPLE) Example JS command that sets various data types to keys"
|
||||||
|
|
||||||
|
// Whether the command should be synced across the RAFT cluster.
|
||||||
|
var sync = true
|
||||||
|
|
||||||
|
/**
|
||||||
|
* keyExtractionFunc is a function that extracts the keys from the command and returns them to SugarDB.keyExtractionFunc
|
||||||
|
* The returned data from this function is used in the Access Control Layer to determine if the current connection is
|
||||||
|
* authorized to execute this command. The function must return a table that specifies which keys in this command
|
||||||
|
* are read keys and which ones are write keys.
|
||||||
|
* Example return: {readKeys: ["key1", "key2"], writeKeys: ["key3", "key4", "key5"]}
|
||||||
|
*
|
||||||
|
* 1. "command" is a string array representing the command that triggered this key extraction function.
|
||||||
|
*
|
||||||
|
* 2. "args" is a string array of the modifier args that were passed when loading the module into SugarDB.
|
||||||
|
* These args are passed to the key extraction function everytime it's invoked.
|
||||||
|
*/
|
||||||
|
function keyExtractionFunc(command, args) {
|
||||||
|
if (command.length > 1) {
|
||||||
|
throw "wrong number of args, expected 0"
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
readKeys: [],
|
||||||
|
writeKeys: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* handlerFunc is the command's handler function. The function is passed some arguments that allow it to interact with
|
||||||
|
* SugarDB. The function must return a valid RESP response or throw an error.
|
||||||
|
* The handler function accepts the following args:
|
||||||
|
*
|
||||||
|
* 1. "context" is a table that contains some information about the environment this command has been executed in.
|
||||||
|
* Example: {protocol: 2, database: 0}
|
||||||
|
* This object contains the following properties:
|
||||||
|
* i) protocol - the protocol version of the client that executed the command (either 2 or 3).
|
||||||
|
* ii) database - the active database index of the client that executed the command.
|
||||||
|
*
|
||||||
|
* 2. "command" is the string array representing the command that triggered this handler function.
|
||||||
|
*
|
||||||
|
* 3. "keyExists" is a function that can be called to check if a list of keys exists in the SugarDB store database.
|
||||||
|
* This function accepts a string array of keys to check and returns a table with each key having a corresponding
|
||||||
|
* boolean value indicating whether it exists.
|
||||||
|
* Examples:
|
||||||
|
* i) Example invocation: keyExists(["key1", "key2", "key3"])
|
||||||
|
* ii) Example return: {key1: true, key2: false, key3: true}
|
||||||
|
*
|
||||||
|
* 4. "getValues" is a function that can be called to retrieve values from the SugarDB store database.
|
||||||
|
* The function accepts a string array of keys whose values we would like to fetch, and returns a table with each key
|
||||||
|
* containing the corresponding value from the store.
|
||||||
|
* The possible data types for the values are: number, string, nil, hash, set, zset
|
||||||
|
* Examples:
|
||||||
|
* i) Example invocation: getValues(["key1", "key2", "key3"])
|
||||||
|
* ii) Example return: {key1: 3.142, key2: nil, key3: "Pi"}
|
||||||
|
*
|
||||||
|
* 5. "setValues" is a function that can be called to set values in the active database in the SugarDB store.
|
||||||
|
* This function accepts a table with keys and the corresponding values to set for each key in the active database
|
||||||
|
* in the store.
|
||||||
|
* The accepted data types for the values are: number, string, nil, hash, set, zset.
|
||||||
|
* The setValues function does not return anything.
|
||||||
|
* Examples:
|
||||||
|
* i) Example invocation: setValues({key1: 3.142, key2: nil, key3: "Pi"})
|
||||||
|
*
|
||||||
|
* 6. "args" is a string array of the modifier args passed to the module at load time. These args are passed to the
|
||||||
|
* handler everytime it's invoked.
|
||||||
|
*/
|
||||||
|
function handlerFunc(ctx, command, keysExist, getValues, setValues, args) {
|
||||||
|
// Set various data types to keys
|
||||||
|
var keyValues = {
|
||||||
|
"numberKey": 42,
|
||||||
|
"stringKey": "Hello, SugarDB!",
|
||||||
|
"floatKey": 3.142,
|
||||||
|
"nilKey": null,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the values in the database
|
||||||
|
setValues(keyValues)
|
||||||
|
|
||||||
|
// Verify the values have been set correctly
|
||||||
|
var keysToGet = ["numberKey", "stringKey", "floatKey", "nilKey"]
|
||||||
|
var retrievedValues = getValues(keysToGet)
|
||||||
|
|
||||||
|
// Create an array to track mismatches
|
||||||
|
var mismatches = [];
|
||||||
|
for (var key in keyValues) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(keyValues, key)) {
|
||||||
|
var expectedValue = keyValues[key];
|
||||||
|
var retrievedValue = retrievedValues[key];
|
||||||
|
if (retrievedValue !== expectedValue) {
|
||||||
|
var msg = "Key " + key + ": expected " + expectedValue + ", got " + retrievedValue
|
||||||
|
mismatches.push(msg);
|
||||||
|
console.log(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If mismatches exist, return an error
|
||||||
|
if (mismatches.length > 0) {
|
||||||
|
throw "values mismatch"
|
||||||
|
}
|
||||||
|
|
||||||
|
// If all values match, return OK
|
||||||
|
return "+OK\r\n"
|
||||||
|
}
|
136
internal/volumes/modules/js/hash.js
Normal file
136
internal/volumes/modules/js/hash.js
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
|
||||||
|
// The keyword to trigger the command
|
||||||
|
var command = "JS.HASH"
|
||||||
|
|
||||||
|
// The string array of categories this command belongs to.
|
||||||
|
// This array can contain both built-in categories and new custom categories.
|
||||||
|
var categories = ["hash", "write", "fast"]
|
||||||
|
|
||||||
|
// The description of the command.
|
||||||
|
var description = "(JS.HASH key) This is an example of working with SugarDB hashes/maps in js scripts."
|
||||||
|
|
||||||
|
// Whether the command should be synced across the RAFT cluster.
|
||||||
|
var sync = true
|
||||||
|
|
||||||
|
/**
|
||||||
|
* keyExtractionFunc is a function that extracts the keys from the command and returns them to SugarDB.keyExtractionFunc
|
||||||
|
* The returned data from this function is used in the Access Control Layer to determine if the current connection is
|
||||||
|
* authorized to execute this command. The function must return a table that specifies which keys in this command
|
||||||
|
* are read keys and which ones are write keys.
|
||||||
|
* Example return: {readKeys: ["key1", "key2"], writeKeys: ["key3", "key4", "key5"]}
|
||||||
|
*
|
||||||
|
* 1. "command" is a string array representing the command that triggered this key extraction function.
|
||||||
|
*
|
||||||
|
* 2. "args" is a string array of the modifier args that were passed when loading the module into SugarDB.
|
||||||
|
* These args are passed to the key extraction function everytime it's invoked.
|
||||||
|
*/
|
||||||
|
function keyExtractionFunc(command, args) {
|
||||||
|
if (command.length !== 2) {
|
||||||
|
throw "wrong number of args, expected 1."
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"readKeys": [],
|
||||||
|
"writeKeys": [command[1]]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* handlerFunc is the command's handler function. The function is passed some arguments that allow it to interact with
|
||||||
|
* SugarDB. The function must return a valid RESP response or throw an error.
|
||||||
|
* The handler function accepts the following args:
|
||||||
|
*
|
||||||
|
* 1. "context" is a table that contains some information about the environment this command has been executed in.
|
||||||
|
* Example: {protocol: 2, database: 0}
|
||||||
|
* This object contains the following properties:
|
||||||
|
* i) protocol - the protocol version of the client that executed the command (either 2 or 3).
|
||||||
|
* ii) database - the active database index of the client that executed the command.
|
||||||
|
*
|
||||||
|
* 2. "command" is the string array representing the command that triggered this handler function.
|
||||||
|
*
|
||||||
|
* 3. "keyExists" is a function that can be called to check if a list of keys exists in the SugarDB store database.
|
||||||
|
* This function accepts a string array of keys to check and returns a table with each key having a corresponding
|
||||||
|
* boolean value indicating whether it exists.
|
||||||
|
* Examples:
|
||||||
|
* i) Example invocation: keyExists(["key1", "key2", "key3"])
|
||||||
|
* ii) Example return: {key1: true, key2: false, key3: true}
|
||||||
|
*
|
||||||
|
* 4. "getValues" is a function that can be called to retrieve values from the SugarDB store database.
|
||||||
|
* The function accepts a string array of keys whose values we would like to fetch, and returns a table with each key
|
||||||
|
* containing the corresponding value from the store.
|
||||||
|
* The possible data types for the values are: number, string, nil, hash, set, zset
|
||||||
|
* Examples:
|
||||||
|
* i) Example invocation: getValues(["key1", "key2", "key3"])
|
||||||
|
* ii) Example return: {key1: 3.142, key2: nil, key3: "Pi"}
|
||||||
|
*
|
||||||
|
* 5. "setValues" is a function that can be called to set values in the active database in the SugarDB store.
|
||||||
|
* This function accepts a table with keys and the corresponding values to set for each key in the active database
|
||||||
|
* in the store.
|
||||||
|
* The accepted data types for the values are: number, string, nil, hash, set, zset.
|
||||||
|
* The setValues function does not return anything.
|
||||||
|
* Examples:
|
||||||
|
* i) Example invocation: setValues({key1: 3.142, key2: nil, key3: "Pi"})
|
||||||
|
*
|
||||||
|
* 6. "args" is a string array of the modifier args passed to the module at load time. These args are passed to the
|
||||||
|
* handler everytime it's invoked.
|
||||||
|
*/
|
||||||
|
function handlerFunc(ctx, command, keysExist, getValues, setValues, args) {
|
||||||
|
// Initialize a new hash
|
||||||
|
var h = new Hash();
|
||||||
|
|
||||||
|
// Set values in the hash
|
||||||
|
h.set({
|
||||||
|
"field1": "value1",
|
||||||
|
"field2": "value2",
|
||||||
|
"field3": "value3",
|
||||||
|
"field4": "value4"
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set hash in the store
|
||||||
|
var setVals = {}
|
||||||
|
setVals[command[1]] = h
|
||||||
|
setValues(setVals);
|
||||||
|
|
||||||
|
// Check that the fields were correctly set in the database
|
||||||
|
var hashValue = getValues([command[1]])[command[1]];
|
||||||
|
console.assert(hashValue.get(["field1"]).field1 === "value1", "field1 not set correctly");
|
||||||
|
console.assert(hashValue.get(["field2"]).field2 === "value2", "field2 not set correctly");
|
||||||
|
console.assert(hashValue.get(["field3"]).field3 === "value3", "field3 not set correctly");
|
||||||
|
console.assert(hashValue.get(["field4"]).field4 === "value4", "field4 not set correctly");
|
||||||
|
|
||||||
|
// Test get method
|
||||||
|
var retrieved = h.get(["field1", "field2"]);
|
||||||
|
console.assert(retrieved.field1 === "value1", "get method failed for field1");
|
||||||
|
console.assert(retrieved.field2 === "value2", "get method failed for field2");
|
||||||
|
|
||||||
|
// Test exists method
|
||||||
|
var exists = h.exists(["field1", "fieldX"]);
|
||||||
|
console.assert(exists.field1 === true, "exists method failed for field1");
|
||||||
|
console.assert(exists.fieldX === false, "exists method failed for fieldX");
|
||||||
|
|
||||||
|
// Test setnx method
|
||||||
|
var setnxCount = h.setnx({
|
||||||
|
"field1": "new_value1", // Should not overwrite
|
||||||
|
"field5": "value5" // Should set
|
||||||
|
});
|
||||||
|
console.assert(setnxCount === 1, "setnx did not set the correct number of fields");
|
||||||
|
console.assert(h.get(["field1"]).field1 === "value1", "setnx overwrote field1");
|
||||||
|
console.assert(h.get(["field5"]).field5 === "value5", "setnx failed to set field5");
|
||||||
|
|
||||||
|
// Test del method
|
||||||
|
var delCount = h.del(["field2", "field3"]);
|
||||||
|
console.assert(delCount === 2, "del did not delete the correct number of fields");
|
||||||
|
console.assert(h.exists(["field2"]).field2 === false, "del failed to delete field2");
|
||||||
|
console.assert(h.exists(["field3"]).field3 === false, "del failed to delete field3");
|
||||||
|
|
||||||
|
// Test len method
|
||||||
|
console.assert(h.len() === 3, "len method returned incorrect value");
|
||||||
|
|
||||||
|
// Retrieve and verify all remaining fields
|
||||||
|
var remainingFields = h.all();
|
||||||
|
console.assert(remainingFields.field1 === "value1", "field1 missing after deletion");
|
||||||
|
console.assert(remainingFields.field4 === "value4", "field4 missing after deletion");
|
||||||
|
console.assert(remainingFields.field5 === "value5", "field5 missing after deletion");
|
||||||
|
|
||||||
|
// Return RESP response
|
||||||
|
return "+OK\r\n";
|
||||||
|
}
|
128
internal/volumes/modules/js/list.js
Normal file
128
internal/volumes/modules/js/list.js
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
|
||||||
|
// The keyword to trigger the command
|
||||||
|
var command = "JS.LIST"
|
||||||
|
|
||||||
|
// The string array of categories this command belongs to.
|
||||||
|
// This array can contain both built-in categories and new custom categories.
|
||||||
|
var categories = ["list", "write", "fast"]
|
||||||
|
|
||||||
|
// The description of the command.
|
||||||
|
var description = "(JS.LIST key) This is an example of working with SugarDB lists in js scripts."
|
||||||
|
|
||||||
|
// Whether the command should be synced across the RAFT cluster.
|
||||||
|
var sync = true
|
||||||
|
|
||||||
|
/**
|
||||||
|
* keyExtractionFunc is a function that extracts the keys from the command and returns them to SugarDB.keyExtractionFunc
|
||||||
|
* The returned data from this function is used in the Access Control Layer to determine if the current connection is
|
||||||
|
* authorized to execute this command. The function must return a table that specifies which keys in this command
|
||||||
|
* are read keys and which ones are write keys.
|
||||||
|
* Example return: {readKeys: ["key1", "key2"], writeKeys: ["key3", "key4", "key5"]}
|
||||||
|
*
|
||||||
|
* 1. "command" is a string array representing the command that triggered this key extraction function.
|
||||||
|
*
|
||||||
|
* 2. "args" is a string array of the modifier args that were passed when loading the module into SugarDB.
|
||||||
|
* These args are passed to the key extraction function everytime it's invoked.
|
||||||
|
*/
|
||||||
|
function keyExtractionFunc(command, args) {
|
||||||
|
if (command.length !== 2) {
|
||||||
|
throw "wrong number of args, expected 4."
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"readKeys": [],
|
||||||
|
"writeKeys": [command[1]]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* handlerFunc is the command's handler function. The function is passed some arguments that allow it to interact with
|
||||||
|
* SugarDB. The function must return a valid RESP response or throw an error.
|
||||||
|
* The handler function accepts the following args:
|
||||||
|
*
|
||||||
|
* 1. "context" is a table that contains some information about the environment this command has been executed in.
|
||||||
|
* Example: {protocol: 2, database: 0}
|
||||||
|
* This object contains the following properties:
|
||||||
|
* i) protocol - the protocol version of the client that executed the command (either 2 or 3).
|
||||||
|
* ii) database - the active database index of the client that executed the command.
|
||||||
|
*
|
||||||
|
* 2. "command" is the string array representing the command that triggered this handler function.
|
||||||
|
*
|
||||||
|
* 3. "keyExists" is a function that can be called to check if a list of keys exists in the SugarDB store database.
|
||||||
|
* This function accepts a string array of keys to check and returns a table with each key having a corresponding
|
||||||
|
* boolean value indicating whether it exists.
|
||||||
|
* Examples:
|
||||||
|
* i) Example invocation: keyExists(["key1", "key2", "key3"])
|
||||||
|
* ii) Example return: {key1: true, key2: false, key3: true}
|
||||||
|
*
|
||||||
|
* 4. "getValues" is a function that can be called to retrieve values from the SugarDB store database.
|
||||||
|
* The function accepts a string array of keys whose values we would like to fetch, and returns a table with each key
|
||||||
|
* containing the corresponding value from the store.
|
||||||
|
* The possible data types for the values are: number, string, nil, hash, set, zset
|
||||||
|
* Examples:
|
||||||
|
* i) Example invocation: getValues(["key1", "key2", "key3"])
|
||||||
|
* ii) Example return: {key1: 3.142, key2: nil, key3: "Pi"}
|
||||||
|
*
|
||||||
|
* 5. "setValues" is a function that can be called to set values in the active database in the SugarDB store.
|
||||||
|
* This function accepts a table with keys and the corresponding values to set for each key in the active database
|
||||||
|
* in the store.
|
||||||
|
* The accepted data types for the values are: number, string, nil, hash, set, zset.
|
||||||
|
* The setValues function does not return anything.
|
||||||
|
* Examples:
|
||||||
|
* i) Example invocation: setValues({key1: 3.142, key2: nil, key3: "Pi"})
|
||||||
|
*
|
||||||
|
* 6. "args" is a string array of the modifier args passed to the module at load time. These args are passed to the
|
||||||
|
* handler everytime it's invoked.
|
||||||
|
*/
|
||||||
|
function handlerFunc(ctx, command, keysExist, getValues, setValues, args) {
|
||||||
|
// Helper function to compare lists
|
||||||
|
function compareLists(expected, actual) {
|
||||||
|
if (expected.length !== actual.length) {
|
||||||
|
return {
|
||||||
|
isValid: false,
|
||||||
|
errorMessage: "Length mismatch: expected " + expected.length + ", got " + actual.length
|
||||||
|
};
|
||||||
|
}
|
||||||
|
for (var i = 0; i < expected.length; i++) {
|
||||||
|
if (expected[i] !== actual[i]) {
|
||||||
|
return {
|
||||||
|
isValid: false,
|
||||||
|
errorMessage: "Mismatch at index " + (i + 1) + ": expected " + expected[i] + ", got " + actual[i]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { isValid: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
var key = command[1]; // Adjusted for JavaScript's 0-based indexing
|
||||||
|
|
||||||
|
// First list to set
|
||||||
|
var initialList = ["apple", "banana", "cherry"];
|
||||||
|
var setVals = {}
|
||||||
|
setVals[key] = initialList
|
||||||
|
setValues(setVals);
|
||||||
|
|
||||||
|
// Retrieve and verify the first list
|
||||||
|
var retrievedValues = getValues([key]);
|
||||||
|
var retrievedList = retrievedValues[key];
|
||||||
|
var result = compareLists(initialList, retrievedList);
|
||||||
|
if (!result.isValid) {
|
||||||
|
throw new Error(result.errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the list with new values
|
||||||
|
var updatedList = ["orange", "grape", "watermelon"];
|
||||||
|
setVals = {}
|
||||||
|
setVals[key] = updatedList
|
||||||
|
setValues(setVals);
|
||||||
|
|
||||||
|
// Retrieve and verify the updated list
|
||||||
|
retrievedValues = getValues([key]);
|
||||||
|
retrievedList = retrievedValues[key];
|
||||||
|
result = compareLists(updatedList, retrievedList);
|
||||||
|
if (!result.isValid) {
|
||||||
|
throw result.errorMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If all assertions pass
|
||||||
|
return "+OK\r\n";
|
||||||
|
}
|
177
internal/volumes/modules/js/set.js
Normal file
177
internal/volumes/modules/js/set.js
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
|
||||||
|
// The keyword to trigger the command
|
||||||
|
var command = "JS.SET"
|
||||||
|
|
||||||
|
// The string array of categories this command belongs to.
|
||||||
|
// This array can contain both built-in categories and new custom categories.
|
||||||
|
var categories = ["set", "write", "fast"]
|
||||||
|
|
||||||
|
// The description of the command.
|
||||||
|
var description = "(JS.SET key member [member ...]]) " +
|
||||||
|
"This is an example of working with SugarDB sets in js scripts."
|
||||||
|
|
||||||
|
// Whether the command should be synced across the RAFT cluster.
|
||||||
|
var sync = true
|
||||||
|
|
||||||
|
/**
|
||||||
|
* keyExtractionFunc is a function that extracts the keys from the command and returns them to SugarDB.keyExtractionFunc
|
||||||
|
* The returned data from this function is used in the Access Control Layer to determine if the current connection is
|
||||||
|
* authorized to execute this command. The function must return a table that specifies which keys in this command
|
||||||
|
* are read keys and which ones are write keys.
|
||||||
|
* Example return: {readKeys: ["key1", "key2"], writeKeys: ["key3", "key4", "key5"]}
|
||||||
|
*
|
||||||
|
* 1. "command" is a string array representing the command that triggered this key extraction function.
|
||||||
|
*
|
||||||
|
* 2. "args" is a string array of the modifier args that were passed when loading the module into SugarDB.
|
||||||
|
* These args are passed to the key extraction function everytime it's invoked.
|
||||||
|
*/
|
||||||
|
function keyExtractionFunc(command, args) {
|
||||||
|
// Check the length of the command array
|
||||||
|
if (command.length < 3) {
|
||||||
|
throw new Error("wrong number of args, expected 2 or more");
|
||||||
|
}
|
||||||
|
// Return the result object
|
||||||
|
return {
|
||||||
|
readKeys: [],
|
||||||
|
writeKeys: [command[1], command[2], command[3]]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* handlerFunc is the command's handler function. The function is passed some arguments that allow it to interact with
|
||||||
|
* SugarDB. The function must return a valid RESP response or throw an error.
|
||||||
|
* The handler function accepts the following args:
|
||||||
|
*
|
||||||
|
* 1. "context" is a table that contains some information about the environment this command has been executed in.
|
||||||
|
* Example: {protocol: 2, database: 0}
|
||||||
|
* This object contains the following properties:
|
||||||
|
* i) protocol - the protocol version of the client that executed the command (either 2 or 3).
|
||||||
|
* ii) database - the active database index of the client that executed the command.
|
||||||
|
*
|
||||||
|
* 2. "command" is the string array representing the command that triggered this handler function.
|
||||||
|
*
|
||||||
|
* 3. "keyExists" is a function that can be called to check if a list of keys exists in the SugarDB store database.
|
||||||
|
* This function accepts a string array of keys to check and returns a table with each key having a corresponding
|
||||||
|
* boolean value indicating whether it exists.
|
||||||
|
* Examples:
|
||||||
|
* i) Example invocation: keyExists(["key1", "key2", "key3"])
|
||||||
|
* ii) Example return: {key1: true, key2: false, key3: true}
|
||||||
|
*
|
||||||
|
* 4. "getValues" is a function that can be called to retrieve values from the SugarDB store database.
|
||||||
|
* The function accepts a string array of keys whose values we would like to fetch, and returns a table with each key
|
||||||
|
* containing the corresponding value from the store.
|
||||||
|
* The possible data types for the values are: number, string, nil, hash, set, zset
|
||||||
|
* Examples:
|
||||||
|
* i) Example invocation: getValues(["key1", "key2", "key3"])
|
||||||
|
* ii) Example return: {key1: 3.142, key2: nil, key3: "Pi"}
|
||||||
|
*
|
||||||
|
* 5. "setValues" is a function that can be called to set values in the active database in the SugarDB store.
|
||||||
|
* This function accepts a table with keys and the corresponding values to set for each key in the active database
|
||||||
|
* in the store.
|
||||||
|
* The accepted data types for the values are: number, string, nil, hash, set, zset.
|
||||||
|
* The setValues function does not return anything.
|
||||||
|
* Examples:
|
||||||
|
* i) Example invocation: setValues({key1: 3.142, key2: nil, key3: "Pi"})
|
||||||
|
*
|
||||||
|
* 6. "args" is a string array of the modifier args passed to the module at load time. These args are passed to the
|
||||||
|
* handler everytime it's invoked.
|
||||||
|
*/
|
||||||
|
function handlerFunc(ctx, command, keysExist, getValues, setValues, args) {
|
||||||
|
// Ensure there are enough arguments
|
||||||
|
if (command.length < 3) {
|
||||||
|
throw "wrong number of arguments, expected at least 3";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the keys
|
||||||
|
var key1 = command[1];
|
||||||
|
var key2 = command[2];
|
||||||
|
var key3 = command[3];
|
||||||
|
|
||||||
|
// Create two sets for testing `move` and `subtract`
|
||||||
|
var set1 = new Set(["elem1", "elem2", "elem3"]);
|
||||||
|
var set2 = new Set(["elem4", "elem5"]);
|
||||||
|
|
||||||
|
// Add elements to set1
|
||||||
|
set1.add(["elem6", "elem7"]);
|
||||||
|
|
||||||
|
// Check if an element exists in set1
|
||||||
|
var containsElem1 = set1.contains("elem1");
|
||||||
|
console.assert(containsElem1, "set1 does not contain expected element elem1")
|
||||||
|
var containsElemUnknown = set1.contains("unknown");
|
||||||
|
console.assert(!containsElemUnknown, "set1 contains unknown element")
|
||||||
|
|
||||||
|
// Get the size of set1
|
||||||
|
var set1Cardinality = set1.cardinality();
|
||||||
|
console.assert(set1Cardinality, "set1 cardinality expected 3, got " + set1Cardinality)
|
||||||
|
|
||||||
|
// Remove elements from set1
|
||||||
|
set1.remove(["elem1", "elem2"]);
|
||||||
|
var removedCount = 2; // Manually track removed count
|
||||||
|
|
||||||
|
// Pop elements from set1
|
||||||
|
set1.add(["elem1", "elem2"]);
|
||||||
|
var poppedElements = set1.pop(2);
|
||||||
|
console.assert(
|
||||||
|
poppedElements.length === 2,
|
||||||
|
"popped elements length must be 2, got " + poppedElements.length
|
||||||
|
)
|
||||||
|
|
||||||
|
// Get random elements from set1
|
||||||
|
var randomElements = set1.random(2);
|
||||||
|
console.assert(
|
||||||
|
randomElements.length === 2,
|
||||||
|
"random elements length must be 2, got " + randomElements.length
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
// Retrieve all elements from set1
|
||||||
|
var allElements = set1.all();
|
||||||
|
console.assert(
|
||||||
|
allElements.length === set1.cardinality(),
|
||||||
|
"all elements length must be " + set1.cardinality() + ", got " + allElements.length
|
||||||
|
)
|
||||||
|
|
||||||
|
// Move an element from set1 to set2
|
||||||
|
set1.add(["elem3"])
|
||||||
|
var moveSuccess = false;
|
||||||
|
if (set1.contains("elem3")) {
|
||||||
|
moveSuccess = set1.move(set2, "elem3");
|
||||||
|
}
|
||||||
|
console.assert(moveSuccess, "element not moved from set1 to set2")
|
||||||
|
|
||||||
|
// Verify that the element was moved
|
||||||
|
var set2ContainsMoved = set2.contains("elem3");
|
||||||
|
console.assert(set2ContainsMoved, "set2 does not contain expected element after move")
|
||||||
|
var set1NoLongerContainsMoved = !set1.contains("elem3");
|
||||||
|
console.assert(set1NoLongerContainsMoved, "set1 still contains unexpected element after move")
|
||||||
|
|
||||||
|
// Subtract set2 from set1
|
||||||
|
var resultSet = set1.subtract([set2]);
|
||||||
|
|
||||||
|
// Store the modified sets
|
||||||
|
var setVals = {}
|
||||||
|
setVals[key1] = set1
|
||||||
|
setVals[key2] = set2
|
||||||
|
setVals[key3] = resultSet
|
||||||
|
setValues(setVals);
|
||||||
|
|
||||||
|
// Retrieve the sets back to verify storage
|
||||||
|
var storedValues = getValues([key1, key2, key3]);
|
||||||
|
var storedSet1 = storedValues[key1];
|
||||||
|
var storedSet2 = storedValues[key2];
|
||||||
|
var storedResultSet = storedValues[key3];
|
||||||
|
|
||||||
|
// Perform additional checks to ensure consistency
|
||||||
|
if (!storedSet1 || storedSet1.size !== set1.size) {
|
||||||
|
throw "Stored set1 does not match the modified set1";
|
||||||
|
}
|
||||||
|
if (!storedSet2 || storedSet2.size !== set2.size) {
|
||||||
|
throw "Stored set2 does not match the modified set2";
|
||||||
|
}
|
||||||
|
if (!storedResultSet || storedResultSet.size !== resultSet.size) {
|
||||||
|
throw "Stored result set does not match the computed result set";
|
||||||
|
}
|
||||||
|
|
||||||
|
// If all operations succeed, return "OK"
|
||||||
|
return "+OK\r\n";
|
||||||
|
}
|
170
internal/volumes/modules/js/zset.js
Normal file
170
internal/volumes/modules/js/zset.js
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
|
||||||
|
// The keyword to trigger the command
|
||||||
|
var command = "JS.ZSET"
|
||||||
|
|
||||||
|
// The string array of categories this command belongs to.
|
||||||
|
// This array can contain both built-in categories and new custom categories.
|
||||||
|
var categories = ["sortedset", "write", "fast"]
|
||||||
|
|
||||||
|
// The description of the command.
|
||||||
|
var description = "(JS.ZSET key member score [member score ...]) " +
|
||||||
|
"This is an example of working with SugarDB sorted sets in js scripts."
|
||||||
|
|
||||||
|
// Whether the command should be synced across the RAFT cluster.
|
||||||
|
var sync = true
|
||||||
|
|
||||||
|
/**
|
||||||
|
* keyExtractionFunc is a function that extracts the keys from the command and returns them to SugarDB.keyExtractionFunc
|
||||||
|
* The returned data from this function is used in the Access Control Layer to determine if the current connection is
|
||||||
|
* authorized to execute this command. The function must return a table that specifies which keys in this command
|
||||||
|
* are read keys and which ones are write keys.
|
||||||
|
* Example return: {readKeys: ["key1", "key2"], writeKeys: ["key3", "key4", "key5"]}
|
||||||
|
*
|
||||||
|
* 1. "command" is a string array representing the command that triggered this key extraction function.
|
||||||
|
*
|
||||||
|
* 2. "args" is a string array of the modifier args that were passed when loading the module into SugarDB.
|
||||||
|
* These args are passed to the key extraction function everytime it's invoked.
|
||||||
|
*/
|
||||||
|
function keyExtractionFunc(command, args) {
|
||||||
|
if (command.length < 4) {
|
||||||
|
throw "wrong number of args, expected 3 or more";
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
readKeys: [],
|
||||||
|
writeKeys: [command[1], command[2], command[3]]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* handlerFunc is the command's handler function. The function is passed some arguments that allow it to interact with
|
||||||
|
* SugarDB. The function must return a valid RESP response or throw an error.
|
||||||
|
* The handler function accepts the following args:
|
||||||
|
*
|
||||||
|
* 1. "context" is a table that contains some information about the environment this command has been executed in.
|
||||||
|
* Example: {protocol: 2, database: 0}
|
||||||
|
* This object contains the following properties:
|
||||||
|
* i) protocol - the protocol version of the client that executed the command (either 2 or 3).
|
||||||
|
* ii) database - the active database index of the client that executed the command.
|
||||||
|
*
|
||||||
|
* 2. "command" is the string array representing the command that triggered this handler function.
|
||||||
|
*
|
||||||
|
* 3. "keyExists" is a function that can be called to check if a list of keys exists in the SugarDB store database.
|
||||||
|
* This function accepts a string array of keys to check and returns a table with each key having a corresponding
|
||||||
|
* boolean value indicating whether it exists.
|
||||||
|
* Examples:
|
||||||
|
* i) Example invocation: keyExists(["key1", "key2", "key3"])
|
||||||
|
* ii) Example return: {key1: true, key2: false, key3: true}
|
||||||
|
*
|
||||||
|
* 4. "getValues" is a function that can be called to retrieve values from the SugarDB store database.
|
||||||
|
* The function accepts a string array of keys whose values we would like to fetch, and returns a table with each key
|
||||||
|
* containing the corresponding value from the store.
|
||||||
|
* The possible data types for the values are: number, string, nil, hash, set, zset
|
||||||
|
* Examples:
|
||||||
|
* i) Example invocation: getValues(["key1", "key2", "key3"])
|
||||||
|
* ii) Example return: {key1: 3.142, key2: nil, key3: "Pi"}
|
||||||
|
*
|
||||||
|
* 5. "setValues" is a function that can be called to set values in the active database in the SugarDB store.
|
||||||
|
* This function accepts a table with keys and the corresponding values to set for each key in the active database
|
||||||
|
* in the store.
|
||||||
|
* The accepted data types for the values are: number, string, nil, hash, set, zset.
|
||||||
|
* The setValues function does not return anything.
|
||||||
|
* Examples:
|
||||||
|
* i) Example invocation: setValues({key1: 3.142, key2: nil, key3: "Pi"})
|
||||||
|
*
|
||||||
|
* 6. "args" is a string array of the modifier args passed to the module at load time. These args are passed to the
|
||||||
|
* handler everytime it's invoked.
|
||||||
|
*/
|
||||||
|
function handlerFunc(ctx, command, keysExist, getValues, setValues, args) {
|
||||||
|
// Ensure there are enough arguments
|
||||||
|
if (command.length < 4) {
|
||||||
|
throw new Error("wrong number of arguments, expected at least 3");
|
||||||
|
}
|
||||||
|
|
||||||
|
var key1 = command[1];
|
||||||
|
var key2 = "key2";
|
||||||
|
var key3 = "key3";
|
||||||
|
|
||||||
|
// Create `ZMember` instances
|
||||||
|
var member1 = new ZMember({ value: "member1", score: 10 });
|
||||||
|
var member2 = new ZMember({ value: "member2", score: 20 });
|
||||||
|
var member3 = new ZMember({ value: "member3", score: 30 });
|
||||||
|
|
||||||
|
// Create a `ZSet` and add initial members
|
||||||
|
var zset1 = new ZSet(member1, member2);
|
||||||
|
|
||||||
|
// Test `add` method with a new member
|
||||||
|
zset1.add([member3]);
|
||||||
|
|
||||||
|
// Test `update` method by modifying an existing member
|
||||||
|
zset1.update([new ZMember({ value: "member1", score: 15 })]);
|
||||||
|
|
||||||
|
// Test `remove` method
|
||||||
|
zset1.remove("member2");
|
||||||
|
|
||||||
|
// Test `cardinality` method
|
||||||
|
var zset1Cardinality = zset1.cardinality();
|
||||||
|
console.assert(zset1Cardinality === 2, "zset1 expected cardinality is 2, got " + zset1Cardinality)
|
||||||
|
|
||||||
|
// Test `contains` method
|
||||||
|
var containsMember3 = zset1.contains("member3");
|
||||||
|
console.assert(containsMember3, "zset1 does not contain expected member member3")
|
||||||
|
var containsNonExistent = zset1.contains("nonexistent");
|
||||||
|
console.assert(!containsNonExistent, "zset1 contains unexpected element 'nonexistent'")
|
||||||
|
|
||||||
|
// Test `random` method
|
||||||
|
var randomMembers = zset1.random(2);
|
||||||
|
console.assert(
|
||||||
|
randomMembers.length === 2,
|
||||||
|
"zset1 random members result should be length 2, got " + randomMembers.length
|
||||||
|
)
|
||||||
|
|
||||||
|
// Test `all` method
|
||||||
|
var allMembers = zset1.all();
|
||||||
|
console.assert(
|
||||||
|
allMembers.length === zset1.cardinality(),
|
||||||
|
"zset1 'all' did not return expected cardinality of " + zset1.cardinality + ", got " + allMembers.length
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create another `ZSet` to test `subtract` manually
|
||||||
|
var zset2 = new ZSet(new ZMember({ value: "member3", score: 30 }));
|
||||||
|
// Subtract the zset2 from zset1
|
||||||
|
var resultZSet = zset1.subtract([zset2])
|
||||||
|
|
||||||
|
// Store the `ZSet` objects in SugarDB
|
||||||
|
var setVals = {}
|
||||||
|
setVals[key1] = zset1
|
||||||
|
setVals[key2] = zset2
|
||||||
|
setVals[key3] = resultZSet
|
||||||
|
setValues(setVals);
|
||||||
|
|
||||||
|
// Retrieve the stored `ZSet` objects to verify storage
|
||||||
|
var storedValues = getValues([key1, key2, key3]);
|
||||||
|
var storedZset1 = storedValues[key1];
|
||||||
|
var storedZset2 = storedValues[key2];
|
||||||
|
var storedZset3 = storedValues[key3];
|
||||||
|
|
||||||
|
// Perform consistency checks
|
||||||
|
if (!storedZset1 || storedZset1.cardinality() !== zset1.cardinality()) {
|
||||||
|
throw "Stored zset1 does not match the modified zset1";
|
||||||
|
}
|
||||||
|
if (!storedZset2 || storedZset2.cardinality() !== zset2.cardinality()) {
|
||||||
|
throw "Stored zset2 does not match the modified zset2";
|
||||||
|
}
|
||||||
|
if (!storedZset3 || storedZset3.cardinality() !== resultZSet.cardinality()) {
|
||||||
|
throw "Stored result zset does not match the computed result zset"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test `ZMember` methods
|
||||||
|
var memberValue = member1.value();
|
||||||
|
member1.value("updated_member1");
|
||||||
|
var updatedValue = member1.value();
|
||||||
|
console.assert(updatedValue !== memberValue, "updated member value still the same as old value")
|
||||||
|
|
||||||
|
var memberScore = member1.score();
|
||||||
|
member1.score(50);
|
||||||
|
var updatedScore = member1.score();
|
||||||
|
console.assert(updatedScore !== memberScore, "updated member score still the same as old score")
|
||||||
|
|
||||||
|
// Return an "OK" response
|
||||||
|
return "+OK\r\n";
|
||||||
|
}
|
@@ -27,9 +27,6 @@ Example return: {["readKeys"] = {"key1", "key2"}, ["writeKeys"] = {"key3", "key4
|
|||||||
These args are passed to the key extraction function everytime it's invoked.
|
These args are passed to the key extraction function everytime it's invoked.
|
||||||
]]
|
]]
|
||||||
function keyExtractionFunc (command, args)
|
function keyExtractionFunc (command, args)
|
||||||
for k,v in pairs(args) do
|
|
||||||
print(k, v)
|
|
||||||
end
|
|
||||||
if (#command ~= 1) then
|
if (#command ~= 1) then
|
||||||
error("wrong number of args, expected 0")
|
error("wrong number of args, expected 0")
|
||||||
end
|
end
|
||||||
|
@@ -28,9 +28,6 @@ Example return: {["readKeys"] = {"key1", "key2"}, ["writeKeys"] = {"key3", "key4
|
|||||||
These args are passed to the key extraction function everytime it's invoked.
|
These args are passed to the key extraction function everytime it's invoked.
|
||||||
]]
|
]]
|
||||||
function keyExtractionFunc (command, args)
|
function keyExtractionFunc (command, args)
|
||||||
for k,v in pairs(args) do
|
|
||||||
print(k, v)
|
|
||||||
end
|
|
||||||
if (#command < 2) then
|
if (#command < 2) then
|
||||||
error("wrong number of args, expected 1")
|
error("wrong number of args, expected 1")
|
||||||
end
|
end
|
||||||
|
@@ -27,9 +27,6 @@ Example return: {["readKeys"] = {"key1", "key2"}, ["writeKeys"] = {"key3", "key4
|
|||||||
These args are passed to the key extraction function everytime it's invoked.
|
These args are passed to the key extraction function everytime it's invoked.
|
||||||
]]
|
]]
|
||||||
function keyExtractionFunc (command, args)
|
function keyExtractionFunc (command, args)
|
||||||
for k,v in pairs(args) do
|
|
||||||
print(k, v)
|
|
||||||
end
|
|
||||||
if (#command < 2) then
|
if (#command < 2) then
|
||||||
error("wrong number of args, expected 1")
|
error("wrong number of args, expected 1")
|
||||||
end
|
end
|
||||||
|
@@ -28,11 +28,8 @@ Example return: {["readKeys"] = {"key1", "key2"}, ["writeKeys"] = {"key3", "key4
|
|||||||
These args are passed to the key extraction function everytime it's invoked.
|
These args are passed to the key extraction function everytime it's invoked.
|
||||||
]]
|
]]
|
||||||
function keyExtractionFunc (command, args)
|
function keyExtractionFunc (command, args)
|
||||||
for k,v in pairs(args) do
|
if (#command < 3) then
|
||||||
print(k, v)
|
error("wrong number of args, expected 2 or more")
|
||||||
end
|
|
||||||
if (#command < 4) then
|
|
||||||
error("wrong number of args, expected 3")
|
|
||||||
end
|
end
|
||||||
return { ["readKeys"] = {}, ["writeKeys"] = {command[2], command[3], command[4]} }
|
return { ["readKeys"] = {}, ["writeKeys"] = {command[2], command[3], command[4]} }
|
||||||
end
|
end
|
||||||
|
@@ -28,10 +28,7 @@ Example return: {["readKeys"] = {"key1", "key2"}, ["writeKeys"] = {"key3", "key4
|
|||||||
These args are passed to the key extraction function everytime it's invoked.
|
These args are passed to the key extraction function everytime it's invoked.
|
||||||
]]
|
]]
|
||||||
function keyExtractionFunc (command, args)
|
function keyExtractionFunc (command, args)
|
||||||
for k,v in pairs(args) do
|
if (#command < 4) then
|
||||||
print(k, v)
|
|
||||||
end
|
|
||||||
if (#command ~= 4) then
|
|
||||||
error("wrong number of args, expected 2")
|
error("wrong number of args, expected 2")
|
||||||
end
|
end
|
||||||
return { ["readKeys"] = {}, ["writeKeys"] = {command[2], command[3], command[4]} }
|
return { ["readKeys"] = {}, ["writeKeys"] = {command[2], command[3], command[4]} }
|
||||||
|
@@ -391,6 +391,51 @@ func TestSugarDB_Plugins(t *testing.T) {
|
|||||||
want: "OK",
|
want: "OK",
|
||||||
wantErr: nil,
|
wantErr: nil,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "9. Test JS module that handles primitive types",
|
||||||
|
path: path.Join("..", "internal", "volumes", "modules", "js", "example.js"),
|
||||||
|
expect: true,
|
||||||
|
args: []string{},
|
||||||
|
cmd: []string{"JS.EXAMPLE"},
|
||||||
|
want: "OK",
|
||||||
|
wantErr: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "10. Test JS module that handles hashes",
|
||||||
|
path: path.Join("..", "internal", "volumes", "modules", "js", "hash.js"),
|
||||||
|
expect: true,
|
||||||
|
args: []string{},
|
||||||
|
cmd: []string{"JS.HASH", "JS_HASH_KEY1"},
|
||||||
|
want: "OK",
|
||||||
|
wantErr: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "11. Test JS module that handles sets",
|
||||||
|
path: path.Join("..", "internal", "volumes", "modules", "js", "set.js"),
|
||||||
|
expect: true,
|
||||||
|
args: []string{},
|
||||||
|
cmd: []string{"JS.SET", "JS_SET_KEY1", "member1"},
|
||||||
|
want: "OK",
|
||||||
|
wantErr: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "12. Test JS module that handles sorted sets",
|
||||||
|
path: path.Join("..", "internal", "volumes", "modules", "js", "zset.js"),
|
||||||
|
expect: true,
|
||||||
|
args: []string{},
|
||||||
|
cmd: []string{"JS.ZSET", "JS_ZSET_KEY1", "member1", "2.142"},
|
||||||
|
want: "OK",
|
||||||
|
wantErr: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "13. Test JS module that handles lists",
|
||||||
|
path: path.Join("..", "internal", "volumes", "modules", "js", "list.js"),
|
||||||
|
expect: true,
|
||||||
|
args: []string{},
|
||||||
|
cmd: []string{"JS.LIST", "JS_LIST_KEY1"},
|
||||||
|
want: "OK",
|
||||||
|
wantErr: nil,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
|
@@ -69,7 +69,6 @@ func (server *SugarDB) getHandlerFuncParams(ctx context.Context, cmd []string, c
|
|||||||
SwapDBs: server.SwapDBs,
|
SwapDBs: server.SwapDBs,
|
||||||
GetServerInfo: server.GetServerInfo,
|
GetServerInfo: server.GetServerInfo,
|
||||||
AddScript: server.AddScript,
|
AddScript: server.AddScript,
|
||||||
AddScriptCommand: server.AddScriptCommand,
|
|
||||||
DeleteKey: func(ctx context.Context, key string) error {
|
DeleteKey: func(ctx context.Context, key string) error {
|
||||||
server.storeLock.Lock()
|
server.storeLock.Lock()
|
||||||
defer server.storeLock.Unlock()
|
defer server.storeLock.Unlock()
|
||||||
@@ -160,8 +159,8 @@ func (server *SugarDB) handleCommand(ctx context.Context, message []byte, conn *
|
|||||||
}
|
}
|
||||||
|
|
||||||
if conn != nil && server.acl != nil && !embedded {
|
if conn != nil && server.acl != nil && !embedded {
|
||||||
// Authorize connection if it's provided and if ACL module is present
|
// Authorize connection if it's provided and if ACL module is present and the embedded parameter is false.
|
||||||
// and the embedded parameter is false.
|
// Skip the authorization if the command is being executed from embedded mode.
|
||||||
if err = server.acl.AuthorizeConnection(conn, cmd, command, subCommand); err != nil {
|
if err = server.acl.AuthorizeConnection(conn, cmd, command, subCommand); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@@ -39,10 +39,12 @@ func (server *SugarDB) AddScriptCommand(
|
|||||||
var engine string
|
var engine string
|
||||||
if strings.HasSuffix(path, ".lua") {
|
if strings.HasSuffix(path, ".lua") {
|
||||||
engine = "lua"
|
engine = "lua"
|
||||||
|
} else if strings.HasSuffix(path, ".js") {
|
||||||
|
engine = "js"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the engine is supported
|
// Check if the engine is supported
|
||||||
supportedEngines := []string{"lua"}
|
supportedEngines := []string{"lua", "js"}
|
||||||
if !slices.Contains(supportedEngines, strings.ToLower(engine)) {
|
if !slices.Contains(supportedEngines, strings.ToLower(engine)) {
|
||||||
return fmt.Errorf("engine %s not supported, only %v engines are supported", engine, supportedEngines)
|
return fmt.Errorf("engine %s not supported, only %v engines are supported", engine, supportedEngines)
|
||||||
}
|
}
|
||||||
@@ -59,6 +61,8 @@ func (server *SugarDB) AddScriptCommand(
|
|||||||
switch strings.ToLower(engine) {
|
switch strings.ToLower(engine) {
|
||||||
case "lua":
|
case "lua":
|
||||||
vm, commandName, categories, description, synchronize, commandType, err = generateLuaCommandInfo(path)
|
vm, commandName, categories, description, synchronize, commandType, err = generateLuaCommandInfo(path)
|
||||||
|
case "js":
|
||||||
|
vm, commandName, categories, description, synchronize, commandType, err = generateJSCommandInfo(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -85,7 +89,7 @@ func (server *SugarDB) AddScriptCommand(
|
|||||||
Description: description,
|
Description: description,
|
||||||
Sync: synchronize,
|
Sync: synchronize,
|
||||||
Type: commandType,
|
Type: commandType,
|
||||||
KeyExtractionFunc: func(engine string, vm any, args []string) internal.KeyExtractionFunc {
|
KeyExtractionFunc: func(engine string, args []string) internal.KeyExtractionFunc {
|
||||||
// Wrapper for the key function
|
// Wrapper for the key function
|
||||||
return func(cmd []string) (internal.KeyExtractionFuncResult, error) {
|
return func(cmd []string) (internal.KeyExtractionFuncResult, error) {
|
||||||
switch strings.ToLower(engine) {
|
switch strings.ToLower(engine) {
|
||||||
@@ -96,21 +100,25 @@ func (server *SugarDB) AddScriptCommand(
|
|||||||
WriteKeys: make([]string, 0),
|
WriteKeys: make([]string, 0),
|
||||||
}, nil
|
}, nil
|
||||||
case "lua":
|
case "lua":
|
||||||
return server.buildLuaKeyExtractionFunc(vm, cmd, args)
|
return server.luaKeyExtractionFunc(cmd, args)
|
||||||
|
case "js":
|
||||||
|
return server.jsKeyExtractionFunc(cmd, args)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}(engine, vm, args),
|
}(engine, args),
|
||||||
HandlerFunc: func(engine string, vm any, args []string) internal.HandlerFunc {
|
HandlerFunc: func(engine string, args []string) internal.HandlerFunc {
|
||||||
// Wrapper that generates handler function
|
// Wrapper that generates handler function
|
||||||
return func(params internal.HandlerFuncParams) ([]byte, error) {
|
return func(params internal.HandlerFuncParams) ([]byte, error) {
|
||||||
switch strings.ToLower(engine) {
|
switch strings.ToLower(engine) {
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("command %s handler not implemented", commandName)
|
return nil, fmt.Errorf("command %s handler not implemented", commandName)
|
||||||
case "lua":
|
case "lua":
|
||||||
return server.buildLuaHandlerFunc(vm, commandName, args, params)
|
return server.luaHandlerFunc(commandName, args, params)
|
||||||
|
case "js":
|
||||||
|
return server.jsHandlerFunc(commandName, args, params)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}(engine, vm, args),
|
}(engine, args),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the commands to the list of commands.
|
// Add the commands to the list of commands.
|
||||||
@@ -131,7 +139,7 @@ func (server *SugarDB) LoadModule(path string, args ...string) error {
|
|||||||
server.commandsRWMut.Lock()
|
server.commandsRWMut.Lock()
|
||||||
defer server.commandsRWMut.Unlock()
|
defer server.commandsRWMut.Unlock()
|
||||||
|
|
||||||
for _, suffix := range []string{".lua"} {
|
for _, suffix := range []string{".lua", ".js"} {
|
||||||
if strings.HasSuffix(path, suffix) {
|
if strings.HasSuffix(path, suffix) {
|
||||||
return server.AddScriptCommand(path, args)
|
return server.AddScriptCommand(path, args)
|
||||||
}
|
}
|
||||||
|
908
sugardb/plugin_javascript.go
Normal file
908
sugardb/plugin_javascript.go
Normal file
@@ -0,0 +1,908 @@
|
|||||||
|
// Copyright 2024 Kelvin Clement Mwinuka
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package sugardb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/echovault/sugardb/internal"
|
||||||
|
"github.com/echovault/sugardb/internal/modules/hash"
|
||||||
|
"github.com/echovault/sugardb/internal/modules/set"
|
||||||
|
"github.com/echovault/sugardb/internal/modules/sorted_set"
|
||||||
|
"github.com/robertkrimen/otto"
|
||||||
|
"math"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
objectRegistry sync.Map
|
||||||
|
idCounter uint64
|
||||||
|
)
|
||||||
|
|
||||||
|
func registerObject(object interface{}) string {
|
||||||
|
id := fmt.Sprintf("id-%d", atomic.AddUint64(&idCounter, 1))
|
||||||
|
objectRegistry.Store(id, object)
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
func getObjectById(id string) (interface{}, bool) {
|
||||||
|
return objectRegistry.Load(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func clearObjectRegistry() {
|
||||||
|
atomic.StoreUint64(&idCounter, 0)
|
||||||
|
objectRegistry.Clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateJSCommandInfo(path string) (*otto.Otto, string, []string, string, bool, string, error) {
|
||||||
|
// Initialize the Otto vm
|
||||||
|
vm := otto.New()
|
||||||
|
|
||||||
|
// Load JS file
|
||||||
|
content, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", nil, "", false, "", fmt.Errorf("could not load javascript script file %s: %v", path, err)
|
||||||
|
}
|
||||||
|
if _, err = vm.Run(content); err != nil {
|
||||||
|
return nil, "", nil, "", false, "", fmt.Errorf("could not run javascript script file %s: %v", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register hash data type
|
||||||
|
_ = vm.Set("Hash", func(call otto.FunctionCall) otto.Value {
|
||||||
|
// Initialize hash
|
||||||
|
h := hash.Hash{}
|
||||||
|
// If an object is passed then initialize the default values of the hash
|
||||||
|
if len(call.ArgumentList) > 0 {
|
||||||
|
args := call.Argument(0).Object()
|
||||||
|
for _, key := range args.Keys() {
|
||||||
|
value, _ := args.Get(key)
|
||||||
|
v, _ := value.ToString()
|
||||||
|
h[key] = hash.HashValue{Value: v}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
obj, _ := call.Otto.Object(`({})`)
|
||||||
|
buildHashObject(obj, h)
|
||||||
|
return obj.Value()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Register set data type
|
||||||
|
_ = vm.Set("Set", func(call otto.FunctionCall) otto.Value {
|
||||||
|
// Initialize set
|
||||||
|
s := set.NewSet([]string{})
|
||||||
|
// If an array is passed add the values to the set
|
||||||
|
if len(call.ArgumentList) > 0 {
|
||||||
|
args := call.Argument(0).Object()
|
||||||
|
var elems []string
|
||||||
|
for _, key := range args.Keys() {
|
||||||
|
value, _ := args.Get(key)
|
||||||
|
v, _ := value.ToString()
|
||||||
|
elems = append(elems, v)
|
||||||
|
}
|
||||||
|
s.Add(elems)
|
||||||
|
}
|
||||||
|
|
||||||
|
obj, _ := call.Otto.Object(`({})`)
|
||||||
|
buildSetObject(obj, s)
|
||||||
|
return obj.Value()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Register sorted set member data type
|
||||||
|
_ = vm.Set("ZMember", func(call otto.FunctionCall) otto.Value {
|
||||||
|
obj, _ := call.Otto.Object(`({})`)
|
||||||
|
|
||||||
|
m := &sorted_set.MemberParam{}
|
||||||
|
if len(call.ArgumentList) != 1 {
|
||||||
|
panicWithFunctionCall(call, "expected an object with score and value properties")
|
||||||
|
}
|
||||||
|
arg := call.Argument(0).Object()
|
||||||
|
// Validate the object
|
||||||
|
if err = validateMemberParamObject(arg); err != nil {
|
||||||
|
panicWithFunctionCall(call, err.Error())
|
||||||
|
}
|
||||||
|
// Get the value
|
||||||
|
value, _ := arg.Get("value")
|
||||||
|
m.Value = sorted_set.Value(value.String())
|
||||||
|
// Get the score
|
||||||
|
s, _ := arg.Get("score")
|
||||||
|
score, _ := s.ToFloat()
|
||||||
|
m.Score = sorted_set.Score(score)
|
||||||
|
// Build the Otto member param object
|
||||||
|
buildMemberParamObject(obj, m)
|
||||||
|
return obj.Value()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Register sorted set data type
|
||||||
|
_ = vm.Set("ZSet", func(call otto.FunctionCall) otto.Value {
|
||||||
|
// If default args are passed when initializing sorted set, add them to the member params
|
||||||
|
var params []sorted_set.MemberParam
|
||||||
|
for _, arg := range call.ArgumentList {
|
||||||
|
if !arg.IsObject() {
|
||||||
|
panicWithFunctionCall(call, "zset constructor args must be sorted set members")
|
||||||
|
}
|
||||||
|
id, _ := arg.Object().Get("__id")
|
||||||
|
o, exists := getObjectById(id.String())
|
||||||
|
if !exists {
|
||||||
|
panicWithFunctionCall(call, "unknown object passed to zset constructor")
|
||||||
|
}
|
||||||
|
p, ok := o.(*sorted_set.MemberParam)
|
||||||
|
if !ok {
|
||||||
|
panicWithFunctionCall(call, "unknown object passed to createZSet function")
|
||||||
|
}
|
||||||
|
params = append(params, *p)
|
||||||
|
}
|
||||||
|
ss := sorted_set.NewSortedSet(params)
|
||||||
|
|
||||||
|
obj, _ := call.Otto.Object(`({})`)
|
||||||
|
buildSortedSetObject(obj, ss)
|
||||||
|
return obj.Value()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Get the command name
|
||||||
|
v, err := vm.Get("command")
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", nil, "", false, "", fmt.Errorf("could not get javascript command %s: %v", path, err)
|
||||||
|
}
|
||||||
|
command, err := v.ToString()
|
||||||
|
if err != nil || len(command) <= 0 {
|
||||||
|
return nil, "", nil, "", false, "", fmt.Errorf("javascript command not found %s: %v", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the categories
|
||||||
|
v, err = vm.Get("categories")
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", nil, "", false, "", fmt.Errorf("could not get javascript command categories %s: %v", path, err)
|
||||||
|
}
|
||||||
|
isArray, _ := vm.Run(`Array.isArray(categories)`)
|
||||||
|
if ok, _ := isArray.ToBoolean(); !ok {
|
||||||
|
return nil, "", nil, "", false, "", fmt.Errorf("javascript command categories is not an array %s: %v", path, err)
|
||||||
|
}
|
||||||
|
c, _ := v.Export()
|
||||||
|
categories := c.([]string)
|
||||||
|
|
||||||
|
// Get the description
|
||||||
|
v, err = vm.Get("description")
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", nil, "", false, "", fmt.Errorf("could not get javascript command description %s: %v", path, err)
|
||||||
|
}
|
||||||
|
description, err := v.ToString()
|
||||||
|
if err != nil || len(description) <= 0 {
|
||||||
|
return nil, "", nil, "", false, "", fmt.Errorf("javascript command description not found %s: %v", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the sync policy
|
||||||
|
v, err = vm.Get("sync")
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", nil, "", false, "", fmt.Errorf("could not get javascript command sync policy %s: %v", path, err)
|
||||||
|
}
|
||||||
|
if !v.IsBoolean() {
|
||||||
|
return nil, "", nil, "", false, "", fmt.Errorf("javascript command sync policy is not a boolean %s: %v", path, err)
|
||||||
|
}
|
||||||
|
synchronize, _ := v.ToBoolean()
|
||||||
|
|
||||||
|
// Set command type
|
||||||
|
commandType := "JS_SCRIPT"
|
||||||
|
|
||||||
|
return vm, strings.ToLower(command), categories, description, synchronize, commandType, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// jsKeyExtractionFunc executes the extraction function defined in the script and returns the result or error.
|
||||||
|
func (server *SugarDB) jsKeyExtractionFunc(cmd []string, args []string) (internal.KeyExtractionFuncResult, error) {
|
||||||
|
// Lock the script before executing the key extraction function.
|
||||||
|
script, ok := server.scriptVMs.Load(strings.ToLower(cmd[0]))
|
||||||
|
if !ok {
|
||||||
|
return internal.KeyExtractionFuncResult{}, fmt.Errorf("no lock found for script command %s", cmd[0])
|
||||||
|
}
|
||||||
|
machine := script.(struct {
|
||||||
|
vm any
|
||||||
|
lock *sync.Mutex
|
||||||
|
})
|
||||||
|
machine.lock.Lock()
|
||||||
|
defer machine.lock.Unlock()
|
||||||
|
|
||||||
|
vm := machine.vm.(*otto.Otto)
|
||||||
|
|
||||||
|
f, _ := vm.Get("keyExtractionFunc")
|
||||||
|
if !f.IsFunction() {
|
||||||
|
return internal.KeyExtractionFuncResult{}, errors.New("keyExtractionFunc is not a function")
|
||||||
|
}
|
||||||
|
v, err := f.Call(f, cmd, args)
|
||||||
|
if err != nil {
|
||||||
|
return internal.KeyExtractionFuncResult{}, err
|
||||||
|
}
|
||||||
|
if !v.IsObject() {
|
||||||
|
return internal.KeyExtractionFuncResult{}, errors.New("keyExtractionFunc return type is not an object")
|
||||||
|
}
|
||||||
|
data := v.Object()
|
||||||
|
|
||||||
|
rk, _ := data.Get("readKeys")
|
||||||
|
rkv, _ := rk.Export()
|
||||||
|
readKeys, ok := rkv.([]string)
|
||||||
|
if !ok {
|
||||||
|
if _, ok = rkv.([]interface{}); !ok {
|
||||||
|
return internal.KeyExtractionFuncResult{}, fmt.Errorf("readKeys for command %s is not an array", cmd[0])
|
||||||
|
}
|
||||||
|
readKeys = []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
wk, _ := data.Get("writeKeys")
|
||||||
|
wkv, _ := wk.Export()
|
||||||
|
writeKeys, ok := wkv.([]string)
|
||||||
|
if !ok {
|
||||||
|
if _, ok = wkv.([]interface{}); !ok {
|
||||||
|
return internal.KeyExtractionFuncResult{}, fmt.Errorf("writeKeys for command %s is not an array", cmd[0])
|
||||||
|
}
|
||||||
|
writeKeys = []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return internal.KeyExtractionFuncResult{
|
||||||
|
Channels: make([]string, 0),
|
||||||
|
ReadKeys: readKeys,
|
||||||
|
WriteKeys: writeKeys,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// jsHandlerFunc executes the extraction function defined in the script nad returns the RESP response or error.
|
||||||
|
func (server *SugarDB) jsHandlerFunc(command string, args []string, params internal.HandlerFuncParams) ([]byte, error) {
|
||||||
|
// Lock the script before executing the key extraction function.
|
||||||
|
script, ok := server.scriptVMs.Load(strings.ToLower(command))
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("no lock found for script command %s", command)
|
||||||
|
}
|
||||||
|
machine := script.(struct {
|
||||||
|
vm any
|
||||||
|
lock *sync.Mutex
|
||||||
|
})
|
||||||
|
machine.lock.Lock()
|
||||||
|
defer machine.lock.Unlock()
|
||||||
|
|
||||||
|
vm := machine.vm.(*otto.Otto)
|
||||||
|
|
||||||
|
f, _ := vm.Get("handlerFunc")
|
||||||
|
if !f.IsFunction() {
|
||||||
|
return nil, errors.New("handlerFunc is not a function")
|
||||||
|
}
|
||||||
|
v, err := f.Call(
|
||||||
|
f,
|
||||||
|
|
||||||
|
// Build context
|
||||||
|
func() otto.Value {
|
||||||
|
obj, _ := vm.Object(`({})`)
|
||||||
|
_ = obj.Set("protocol", params.Context.Value("Protocol").(int))
|
||||||
|
_ = obj.Set("database", params.Context.Value("Database").(int))
|
||||||
|
return obj.Value()
|
||||||
|
}(),
|
||||||
|
|
||||||
|
// Command
|
||||||
|
params.Command,
|
||||||
|
|
||||||
|
// Build keysExist function
|
||||||
|
func(keys []string) otto.Value {
|
||||||
|
obj, _ := vm.Object(`({})`)
|
||||||
|
exists := server.keysExist(params.Context, keys)
|
||||||
|
for key, value := range exists {
|
||||||
|
_ = obj.Set(key, value)
|
||||||
|
}
|
||||||
|
return obj.Value()
|
||||||
|
},
|
||||||
|
|
||||||
|
// Build getValues function
|
||||||
|
func(keys []string) otto.Value {
|
||||||
|
obj, _ := vm.Object(`({})`)
|
||||||
|
values := server.getValues(params.Context, keys)
|
||||||
|
for key, value := range values {
|
||||||
|
switch value.(type) {
|
||||||
|
default:
|
||||||
|
_ = obj.Set(key, value)
|
||||||
|
case nil:
|
||||||
|
_ = obj.Set(key, otto.NullValue())
|
||||||
|
case []string:
|
||||||
|
l, _ := vm.Object(`([])`)
|
||||||
|
for i, elem := range value.([]string) {
|
||||||
|
_ = l.Set(fmt.Sprintf("%d", i), elem)
|
||||||
|
}
|
||||||
|
_ = obj.Set(key, l.Value())
|
||||||
|
case hash.Hash:
|
||||||
|
h, _ := vm.Object(`({})`)
|
||||||
|
buildHashObject(h, value.(hash.Hash))
|
||||||
|
_ = obj.Set(key, h.Value())
|
||||||
|
case *set.Set:
|
||||||
|
s, _ := vm.Object(`({})`)
|
||||||
|
buildSetObject(s, value.(*set.Set))
|
||||||
|
_ = obj.Set(key, s.Value())
|
||||||
|
case *sorted_set.SortedSet:
|
||||||
|
ss, _ := vm.Object(`({})`)
|
||||||
|
buildSortedSetObject(ss, value.(*sorted_set.SortedSet))
|
||||||
|
_ = obj.Set(key, ss.Value())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return obj.Value()
|
||||||
|
},
|
||||||
|
|
||||||
|
// Build setValues function
|
||||||
|
func(entries map[string]interface{}) {
|
||||||
|
values := make(map[string]interface{})
|
||||||
|
for key, entry := range entries {
|
||||||
|
switch entry.(type) {
|
||||||
|
default:
|
||||||
|
panicInHandler(fmt.Sprintf("unknown type %s on key %s", reflect.TypeOf(entry).String(), key))
|
||||||
|
case nil:
|
||||||
|
values[key] = nil
|
||||||
|
case string:
|
||||||
|
values[key] = internal.AdaptType(entry.(string))
|
||||||
|
case int64:
|
||||||
|
values[key] = int(entry.(int64))
|
||||||
|
case float64:
|
||||||
|
values[key] = entry.(float64)
|
||||||
|
case []string:
|
||||||
|
values[key] = entry.([]string)
|
||||||
|
case map[string]interface{}:
|
||||||
|
value, ok := entry.(map[string]interface{})
|
||||||
|
if !ok || value["__id"] == nil {
|
||||||
|
panicInHandler(fmt.Sprintf("unknown object on key %s", key))
|
||||||
|
}
|
||||||
|
obj, exists := getObjectById(value["__id"].(string))
|
||||||
|
if !exists {
|
||||||
|
panicInHandler(
|
||||||
|
fmt.Sprintf(
|
||||||
|
"could not find object of id %s in the object registry on key %s",
|
||||||
|
value["__id"].(string),
|
||||||
|
key,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
switch obj.(type) {
|
||||||
|
default:
|
||||||
|
panicInHandler(fmt.Sprintf("unknown type on key %s for command %s\n", key, command))
|
||||||
|
case hash.Hash:
|
||||||
|
values[key] = obj.(hash.Hash)
|
||||||
|
case *set.Set:
|
||||||
|
values[key] = obj.(*set.Set)
|
||||||
|
case *sorted_set.SortedSet:
|
||||||
|
values[key] = obj.(*sorted_set.SortedSet)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := server.setValues(params.Context, values); err != nil {
|
||||||
|
panicInHandler(err.Error())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Args
|
||||||
|
args,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
res, err := v.ToString()
|
||||||
|
|
||||||
|
clearObjectRegistry()
|
||||||
|
|
||||||
|
return []byte(res), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildHashObject(obj *otto.Object, h hash.Hash) {
|
||||||
|
_ = obj.Set("__type", "hash")
|
||||||
|
_ = obj.Set("__id", registerObject(h))
|
||||||
|
_ = obj.Set("set", func(call otto.FunctionCall) otto.Value {
|
||||||
|
args := call.Argument(0).Object()
|
||||||
|
for _, key := range args.Keys() {
|
||||||
|
value, _ := args.Get(key)
|
||||||
|
v, _ := value.ToString()
|
||||||
|
h[key] = hash.HashValue{Value: v}
|
||||||
|
}
|
||||||
|
// Return changed count using the set data type
|
||||||
|
count, _ := otto.ToValue(set.NewSet(args.Keys()).Cardinality())
|
||||||
|
return count
|
||||||
|
})
|
||||||
|
_ = obj.Set("setnx", func(call otto.FunctionCall) otto.Value {
|
||||||
|
count := 0
|
||||||
|
args := call.Argument(0).Object()
|
||||||
|
for _, key := range args.Keys() {
|
||||||
|
if _, exists := h[key]; exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
count += 1
|
||||||
|
value, _ := args.Get(key)
|
||||||
|
v, _ := value.ToString()
|
||||||
|
h[key] = hash.HashValue{Value: v}
|
||||||
|
}
|
||||||
|
c, _ := otto.ToValue(count)
|
||||||
|
return c
|
||||||
|
})
|
||||||
|
_ = obj.Set("get", func(call otto.FunctionCall) otto.Value {
|
||||||
|
result, _ := call.Otto.Object(`({})`)
|
||||||
|
for _, arg := range call.ArgumentList {
|
||||||
|
key, _ := arg.ToString()
|
||||||
|
value, _ := otto.ToValue(h[key].Value)
|
||||||
|
_ = result.Set(key, value)
|
||||||
|
}
|
||||||
|
return result.Value()
|
||||||
|
})
|
||||||
|
_ = obj.Set("len", func(call otto.FunctionCall) otto.Value {
|
||||||
|
length, _ := otto.ToValue(len(h))
|
||||||
|
return length
|
||||||
|
})
|
||||||
|
_ = obj.Set("all", func(call otto.FunctionCall) otto.Value {
|
||||||
|
result, _ := call.Otto.Object(`({})`)
|
||||||
|
for key, value := range h {
|
||||||
|
v, _ := otto.ToValue(value.Value)
|
||||||
|
_ = result.Set(key, v)
|
||||||
|
}
|
||||||
|
return result.Value()
|
||||||
|
})
|
||||||
|
_ = obj.Set("exists", func(call otto.FunctionCall) otto.Value {
|
||||||
|
result, _ := call.Otto.Object(`({})`)
|
||||||
|
for _, arg := range call.ArgumentList {
|
||||||
|
key, _ := arg.ToString()
|
||||||
|
_, ok := h[key]
|
||||||
|
exists, _ := call.Otto.ToValue(ok)
|
||||||
|
_ = result.Set(key, exists)
|
||||||
|
}
|
||||||
|
return result.Value()
|
||||||
|
})
|
||||||
|
_ = obj.Set("del", func(call otto.FunctionCall) otto.Value {
|
||||||
|
count := 0
|
||||||
|
for _, arg := range call.ArgumentList {
|
||||||
|
key, _ := arg.ToString()
|
||||||
|
if _, exists := h[key]; exists {
|
||||||
|
count += 1
|
||||||
|
delete(h, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result, _ := otto.ToValue(count)
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildSetObject(obj *otto.Object, s *set.Set) {
|
||||||
|
_ = obj.Set("__type", "set")
|
||||||
|
_ = obj.Set("__id", registerObject(s))
|
||||||
|
_ = obj.Set("add", func(call otto.FunctionCall) otto.Value {
|
||||||
|
args := call.Argument(0).Object()
|
||||||
|
if args == nil {
|
||||||
|
panicWithFunctionCall(call, "set add method argument not an object")
|
||||||
|
}
|
||||||
|
var elems []string
|
||||||
|
for _, key := range args.Keys() {
|
||||||
|
value, _ := args.Get(key)
|
||||||
|
v, _ := value.ToString()
|
||||||
|
elems = append(elems, v)
|
||||||
|
}
|
||||||
|
count := s.Add(elems)
|
||||||
|
result, _ := otto.ToValue(count)
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
_ = obj.Set("pop", func(call otto.FunctionCall) otto.Value {
|
||||||
|
count, _ := call.Argument(0).ToInteger()
|
||||||
|
popped := s.Pop(int(count))
|
||||||
|
result, _ := call.Otto.Object(`([])`)
|
||||||
|
_ = result.Set("length", len(popped))
|
||||||
|
for i, p := range popped {
|
||||||
|
_ = result.Set(fmt.Sprintf("%d", i), p)
|
||||||
|
}
|
||||||
|
return result.Value()
|
||||||
|
})
|
||||||
|
_ = obj.Set("contains", func(call otto.FunctionCall) otto.Value {
|
||||||
|
value, _ := call.Argument(0).ToString()
|
||||||
|
result, _ := otto.ToValue(s.Contains(value))
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
_ = obj.Set("cardinality", func(call otto.FunctionCall) otto.Value {
|
||||||
|
result, _ := otto.ToValue(s.Cardinality())
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
_ = obj.Set("remove", func(call otto.FunctionCall) otto.Value {
|
||||||
|
args := call.Argument(0).Object()
|
||||||
|
if args == nil {
|
||||||
|
panicWithFunctionCall(call, "set remove method argument not an object")
|
||||||
|
}
|
||||||
|
var elems []string
|
||||||
|
for _, key := range args.Keys() {
|
||||||
|
value, _ := args.Get(key)
|
||||||
|
v, _ := value.ToString()
|
||||||
|
elems = append(elems, v)
|
||||||
|
}
|
||||||
|
result, _ := otto.ToValue(s.Remove(elems))
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
_ = obj.Set("all", func(call otto.FunctionCall) otto.Value {
|
||||||
|
all := s.GetAll()
|
||||||
|
result, _ := call.Otto.Object(`([])`)
|
||||||
|
_ = result.Set("length", len(all))
|
||||||
|
for i, e := range all {
|
||||||
|
_ = result.Set(fmt.Sprintf("%d", i), e)
|
||||||
|
}
|
||||||
|
return result.Value()
|
||||||
|
})
|
||||||
|
_ = obj.Set("random", func(call otto.FunctionCall) otto.Value {
|
||||||
|
count, _ := call.Argument(0).ToInteger()
|
||||||
|
random := s.GetRandom(int(count))
|
||||||
|
result, _ := call.Otto.Object(`([])`)
|
||||||
|
_ = result.Set("length", len(random))
|
||||||
|
for i, r := range random {
|
||||||
|
_ = result.Set(fmt.Sprintf("%d", i), r)
|
||||||
|
}
|
||||||
|
return result.Value()
|
||||||
|
})
|
||||||
|
_ = obj.Set("move", func(call otto.FunctionCall) otto.Value {
|
||||||
|
arg := call.Argument(0).Object()
|
||||||
|
elem := call.Argument(1).String()
|
||||||
|
id, _ := arg.Get("__id")
|
||||||
|
o, exists := getObjectById(id.String())
|
||||||
|
if !exists {
|
||||||
|
panicWithFunctionCall(call, "move target set does not exist")
|
||||||
|
}
|
||||||
|
switch o.(type) {
|
||||||
|
default:
|
||||||
|
panicWithFunctionCall(call, "move target is not a set")
|
||||||
|
case *set.Set:
|
||||||
|
moved := s.Move(o.(*set.Set), elem) == 1
|
||||||
|
result, _ := otto.ToValue(moved)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
return otto.NullValue()
|
||||||
|
})
|
||||||
|
_ = obj.Set("subtract", func(call otto.FunctionCall) otto.Value {
|
||||||
|
extractSets := func(call otto.FunctionCall) ([]*set.Set, error) {
|
||||||
|
var sets []*set.Set
|
||||||
|
if len(call.ArgumentList) > 1 {
|
||||||
|
return sets, fmt.Errorf("set subtract method expects 1 arg, got %d", len(call.ArgumentList))
|
||||||
|
}
|
||||||
|
arg1 := call.Argument(0).Object()
|
||||||
|
if arg1.Class() != "Array" {
|
||||||
|
return sets, errors.New("set subtract method expects the first argument to be an array")
|
||||||
|
}
|
||||||
|
for _, key := range arg1.Keys() {
|
||||||
|
// Check if the array element is a valid MemberParam type.
|
||||||
|
argMember, _ := arg1.Get(key)
|
||||||
|
if !argMember.IsObject() {
|
||||||
|
panicWithFunctionCall(call, "set subtract method first arg must be an array of valid sets")
|
||||||
|
}
|
||||||
|
// Get the member param from the object registry
|
||||||
|
argMemberObj := argMember.Object()
|
||||||
|
id, _ := argMemberObj.Get("__id")
|
||||||
|
o, exists := getObjectById(id.String())
|
||||||
|
if !exists {
|
||||||
|
panicWithFunctionCall(call, "set subtract method first arg must be an array of valid sets")
|
||||||
|
}
|
||||||
|
m, ok := o.(*set.Set)
|
||||||
|
if !ok {
|
||||||
|
panicWithFunctionCall(call, "set subtract method first arg must be an array of valid sets")
|
||||||
|
}
|
||||||
|
sets = append(sets, m)
|
||||||
|
}
|
||||||
|
return sets, nil
|
||||||
|
}
|
||||||
|
sets, err := extractSets(call)
|
||||||
|
if err != nil {
|
||||||
|
panicWithFunctionCall(call, err.Error())
|
||||||
|
}
|
||||||
|
diff := s.Subtract(sets)
|
||||||
|
result, _ := call.Otto.Object(`({})`)
|
||||||
|
buildSetObject(result, diff)
|
||||||
|
return result.Value()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildMemberParamObject(obj *otto.Object, m *sorted_set.MemberParam) {
|
||||||
|
_ = obj.Set("__type", "zmember")
|
||||||
|
_ = obj.Set("__id", registerObject(m))
|
||||||
|
_ = obj.Set("value", func(call otto.FunctionCall) otto.Value {
|
||||||
|
switch len(call.ArgumentList) {
|
||||||
|
case 0:
|
||||||
|
// If no value is passed, then return the current value
|
||||||
|
v, _ := otto.ToValue(m.Value)
|
||||||
|
return v
|
||||||
|
case 1:
|
||||||
|
// If a value is passed, then set the value
|
||||||
|
v := call.Argument(0).String()
|
||||||
|
if len(v) <= 0 {
|
||||||
|
panicWithFunctionCall(call, "zset member value must be a non-empty string")
|
||||||
|
}
|
||||||
|
m.Value = sorted_set.Value(v)
|
||||||
|
default:
|
||||||
|
panicWithFunctionCall(
|
||||||
|
call,
|
||||||
|
fmt.Sprintf(
|
||||||
|
"expected either 0 or 1 args for value method of zmember, got %d",
|
||||||
|
len(call.ArgumentList),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return otto.NullValue()
|
||||||
|
})
|
||||||
|
_ = obj.Set("score", func(call otto.FunctionCall) otto.Value {
|
||||||
|
switch len(call.ArgumentList) {
|
||||||
|
case 0:
|
||||||
|
s, _ := otto.ToValue(m.Score)
|
||||||
|
return s
|
||||||
|
case 1:
|
||||||
|
s, _ := call.Argument(0).ToFloat()
|
||||||
|
if math.IsNaN(s) {
|
||||||
|
panicWithFunctionCall(call, "zset member score must be a valid number")
|
||||||
|
}
|
||||||
|
m.Score = sorted_set.Score(s)
|
||||||
|
default:
|
||||||
|
panicWithFunctionCall(
|
||||||
|
call,
|
||||||
|
fmt.Sprintf(
|
||||||
|
"expected either 0 or 1 args for score method of zmember, got %d",
|
||||||
|
len(call.ArgumentList),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return otto.NullValue()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateMemberParamObject(obj *otto.Object) error {
|
||||||
|
value, _ := obj.Get("value")
|
||||||
|
if slices.Contains([]otto.Value{otto.UndefinedValue(), otto.NullValue()}, value) ||
|
||||||
|
len(value.String()) == 0 {
|
||||||
|
return errors.New("zset member value must be a non-empty string")
|
||||||
|
}
|
||||||
|
s, _ := obj.Get("score")
|
||||||
|
if slices.Contains([]otto.Value{otto.UndefinedValue(), otto.NullValue()}, s) {
|
||||||
|
return errors.New("zset member must have a score")
|
||||||
|
}
|
||||||
|
score, _ := s.ToFloat()
|
||||||
|
if math.IsNaN(score) {
|
||||||
|
return errors.New("zset member score must be a valid number")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildSortedSetObject(obj *otto.Object, ss *sorted_set.SortedSet) {
|
||||||
|
// Function to extract member param arguments for "add" and "update" methods.
|
||||||
|
extractMembers := func(call otto.FunctionCall) ([]sorted_set.MemberParam, error) {
|
||||||
|
var members []sorted_set.MemberParam
|
||||||
|
if !call.Argument(0).IsObject() {
|
||||||
|
return members, errors.New("zset add or update method expects the first argument to be an array")
|
||||||
|
}
|
||||||
|
arg1 := call.Argument(0).Object()
|
||||||
|
if arg1.Class() != "Array" {
|
||||||
|
return members, errors.New("zset add or update method expects the first argument to be an array")
|
||||||
|
}
|
||||||
|
for _, key := range arg1.Keys() {
|
||||||
|
// Check if the array element is a valid MemberParam type.
|
||||||
|
argMember, _ := arg1.Get(key)
|
||||||
|
if !argMember.IsObject() {
|
||||||
|
panicWithFunctionCall(call, "zset add or update method first arg must be an array of valid zmembers")
|
||||||
|
}
|
||||||
|
// Get the member param from the object registry
|
||||||
|
argMemberObj := argMember.Object()
|
||||||
|
id, _ := argMemberObj.Get("__id")
|
||||||
|
o, exists := getObjectById(id.String())
|
||||||
|
if !exists {
|
||||||
|
panicWithFunctionCall(call, "zset add or update method first arg must be an array of valid zmembers")
|
||||||
|
}
|
||||||
|
m, ok := o.(*sorted_set.MemberParam)
|
||||||
|
if !ok {
|
||||||
|
panicWithFunctionCall(call, "zset add or update method first arg must be an array of valid zmembers")
|
||||||
|
}
|
||||||
|
members = append(members, *m)
|
||||||
|
}
|
||||||
|
return members, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to build and verify the update policy for "add" and "update" methods
|
||||||
|
type updateModifiers struct {
|
||||||
|
updatePolicy interface{}
|
||||||
|
comparison interface{}
|
||||||
|
changed interface{}
|
||||||
|
incr interface{}
|
||||||
|
}
|
||||||
|
extractUpdateModifiers := func(call otto.FunctionCall) (updateModifiers, error) {
|
||||||
|
modifiers := updateModifiers{updatePolicy: nil, comparison: nil, changed: nil, incr: nil}
|
||||||
|
if len(call.ArgumentList) < 2 {
|
||||||
|
return modifiers, nil
|
||||||
|
}
|
||||||
|
if !call.Argument(1).IsObject() {
|
||||||
|
return modifiers, errors.New("zset add or update method second arg must be an object")
|
||||||
|
}
|
||||||
|
arg2 := call.Argument(1).Object()
|
||||||
|
acceptedKeys := []string{"exists", "comparison", "changed", "incr"}
|
||||||
|
for _, key := range arg2.Keys() {
|
||||||
|
if !slices.Contains(acceptedKeys, key) {
|
||||||
|
return modifiers, fmt.Errorf(
|
||||||
|
"zset add or update method second arg unknown key '%s', expected %+v", key, acceptedKeys)
|
||||||
|
}
|
||||||
|
v, _ := arg2.Get(key)
|
||||||
|
switch key {
|
||||||
|
case "exists":
|
||||||
|
if !v.IsBoolean() {
|
||||||
|
return modifiers, errors.New("zset add or update method second arg 'exists' key should be a boolean")
|
||||||
|
}
|
||||||
|
exists, _ := v.ToBoolean()
|
||||||
|
if exists {
|
||||||
|
modifiers.updatePolicy = "xx"
|
||||||
|
} else {
|
||||||
|
modifiers.updatePolicy = "nx"
|
||||||
|
}
|
||||||
|
case "comparison":
|
||||||
|
modifiers.comparison = v.String()
|
||||||
|
case "changed":
|
||||||
|
if !v.IsBoolean() {
|
||||||
|
return modifiers, errors.New("zset add or update method second arg 'changed' key should be a boolean")
|
||||||
|
}
|
||||||
|
changed, _ := v.ToBoolean()
|
||||||
|
modifiers.changed = changed
|
||||||
|
case "incr":
|
||||||
|
if !v.IsBoolean() {
|
||||||
|
return modifiers, errors.New("zset add or update method second arg 'incr' key should be a boolean")
|
||||||
|
}
|
||||||
|
incr, _ := v.ToBoolean()
|
||||||
|
modifiers.incr = incr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return modifiers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = obj.Set("__type", "zset")
|
||||||
|
_ = obj.Set("__id", registerObject(ss))
|
||||||
|
_ = obj.Set("add", func(call otto.FunctionCall) otto.Value {
|
||||||
|
if len(call.ArgumentList) < 1 || len(call.ArgumentList) > 2 {
|
||||||
|
panicWithFunctionCall(call, fmt.Sprintf("zset add method expects 1 or 2 args, got %d", len(call.ArgumentList)))
|
||||||
|
}
|
||||||
|
// Extract the member params from the first arg
|
||||||
|
members, err := extractMembers(call)
|
||||||
|
if err != nil {
|
||||||
|
panicWithFunctionCall(call, err.Error())
|
||||||
|
}
|
||||||
|
// Extract the modifiers in the second arg, if they are passed.
|
||||||
|
modifiers, err := extractUpdateModifiers(call)
|
||||||
|
if err != nil {
|
||||||
|
panicWithFunctionCall(call, err.Error())
|
||||||
|
}
|
||||||
|
count, err := ss.AddOrUpdate(
|
||||||
|
members,
|
||||||
|
modifiers.updatePolicy,
|
||||||
|
modifiers.comparison,
|
||||||
|
modifiers.changed,
|
||||||
|
modifiers.incr,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
panicWithFunctionCall(call, err.Error())
|
||||||
|
}
|
||||||
|
v, _ := call.Otto.ToValue(count)
|
||||||
|
return v
|
||||||
|
})
|
||||||
|
_ = obj.Set("update", func(call otto.FunctionCall) otto.Value {
|
||||||
|
if len(call.ArgumentList) < 1 || len(call.ArgumentList) > 2 {
|
||||||
|
panicWithFunctionCall(call, fmt.Sprintf("zset update method expects 1 or 2 args, got %d", len(call.ArgumentList)))
|
||||||
|
}
|
||||||
|
// Extract the member params from the first arg
|
||||||
|
members, err := extractMembers(call)
|
||||||
|
if err != nil {
|
||||||
|
panicWithFunctionCall(call, err.Error())
|
||||||
|
}
|
||||||
|
// Extract the modifiers in the second arg, if they are passed.
|
||||||
|
modifiers, err := extractUpdateModifiers(call)
|
||||||
|
if err != nil {
|
||||||
|
panicWithFunctionCall(call, err.Error())
|
||||||
|
}
|
||||||
|
count, err := ss.AddOrUpdate(
|
||||||
|
members,
|
||||||
|
modifiers.updatePolicy,
|
||||||
|
modifiers.comparison,
|
||||||
|
modifiers.changed,
|
||||||
|
modifiers.incr,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
panicWithFunctionCall(call, err.Error())
|
||||||
|
}
|
||||||
|
v, _ := call.Otto.ToValue(count)
|
||||||
|
return v
|
||||||
|
})
|
||||||
|
_ = obj.Set("remove", func(call otto.FunctionCall) otto.Value {
|
||||||
|
if len(call.ArgumentList) != 1 {
|
||||||
|
panicWithFunctionCall(call, fmt.Sprintf("zset remove method expects 1 ard, got %d", len(call.ArgumentList)))
|
||||||
|
}
|
||||||
|
value := sorted_set.Value(call.Argument(0).String())
|
||||||
|
v, _ := call.Otto.ToValue(ss.Remove(value))
|
||||||
|
return v
|
||||||
|
})
|
||||||
|
_ = obj.Set("cardinality", func(call otto.FunctionCall) otto.Value {
|
||||||
|
value, _ := otto.ToValue(ss.Cardinality())
|
||||||
|
return value
|
||||||
|
})
|
||||||
|
_ = obj.Set("contains", func(call otto.FunctionCall) otto.Value {
|
||||||
|
if len(call.ArgumentList) != 1 {
|
||||||
|
panicWithFunctionCall(call, fmt.Sprintf("zset contains method expects 1 arg, got %d", len(call.ArgumentList)))
|
||||||
|
}
|
||||||
|
v, _ := otto.ToValue(ss.Contains(sorted_set.Value(call.Argument(0).String())))
|
||||||
|
return v
|
||||||
|
})
|
||||||
|
_ = obj.Set("random", func(call otto.FunctionCall) otto.Value {
|
||||||
|
if len(call.ArgumentList) != 1 {
|
||||||
|
panicWithFunctionCall(call, fmt.Sprintf("zset random method expects 1 arg, got %d", len(call.ArgumentList)))
|
||||||
|
}
|
||||||
|
count, _ := call.Argument(0).ToInteger()
|
||||||
|
var paramValues []otto.Value
|
||||||
|
for _, p := range ss.GetRandom(int(count)) {
|
||||||
|
m, _ := call.Otto.Object(`({})`)
|
||||||
|
buildMemberParamObject(m, &p)
|
||||||
|
paramValues = append(paramValues, m.Value())
|
||||||
|
}
|
||||||
|
p, _ := call.Otto.ToValue(paramValues)
|
||||||
|
return p
|
||||||
|
})
|
||||||
|
_ = obj.Set("all", func(call otto.FunctionCall) otto.Value {
|
||||||
|
var paramValues []otto.Value
|
||||||
|
for _, p := range ss.GetAll() {
|
||||||
|
m, _ := call.Otto.Object(`({})`)
|
||||||
|
buildMemberParamObject(m, &p)
|
||||||
|
paramValues = append(paramValues, m.Value())
|
||||||
|
}
|
||||||
|
p, _ := call.Otto.ToValue(paramValues)
|
||||||
|
return p
|
||||||
|
})
|
||||||
|
_ = obj.Set("subtract", func(call otto.FunctionCall) otto.Value {
|
||||||
|
extractZSets := func(call otto.FunctionCall) ([]*sorted_set.SortedSet, error) {
|
||||||
|
var zsets []*sorted_set.SortedSet
|
||||||
|
if len(call.ArgumentList) > 1 {
|
||||||
|
return zsets, fmt.Errorf("zset subtract method expects 1 arg, got %d", len(call.ArgumentList))
|
||||||
|
}
|
||||||
|
arg1 := call.Argument(0).Object()
|
||||||
|
if arg1.Class() != "Array" {
|
||||||
|
return zsets, errors.New("zset subtract method expects the first argument to be an array")
|
||||||
|
}
|
||||||
|
for _, key := range arg1.Keys() {
|
||||||
|
// Check if the array element is a valid MemberParam type.
|
||||||
|
argMember, _ := arg1.Get(key)
|
||||||
|
if !argMember.IsObject() {
|
||||||
|
panicWithFunctionCall(call, "zset subtract method first arg must be an array of valid zsets")
|
||||||
|
}
|
||||||
|
// Get the member param from the object registry
|
||||||
|
argMemberObj := argMember.Object()
|
||||||
|
id, _ := argMemberObj.Get("__id")
|
||||||
|
o, exists := getObjectById(id.String())
|
||||||
|
if !exists {
|
||||||
|
panicWithFunctionCall(call, "zset subtract method first arg must be an array of valid zsets")
|
||||||
|
}
|
||||||
|
m, ok := o.(*sorted_set.SortedSet)
|
||||||
|
if !ok {
|
||||||
|
panicWithFunctionCall(call, "zset subtract method first arg must be an array of valid zsets")
|
||||||
|
}
|
||||||
|
zsets = append(zsets, m)
|
||||||
|
}
|
||||||
|
return zsets, nil
|
||||||
|
}
|
||||||
|
zsets, err := extractZSets(call)
|
||||||
|
if err != nil {
|
||||||
|
panicWithFunctionCall(call, err.Error())
|
||||||
|
}
|
||||||
|
diff := ss.Subtract(zsets)
|
||||||
|
result, _ := call.Otto.Object(`({})`)
|
||||||
|
buildSortedSetObject(result, diff)
|
||||||
|
return result.Value()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func panicWithFunctionCall(call otto.FunctionCall, message string) {
|
||||||
|
err, _ := call.Otto.ToValue(message)
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func panicInHandler(message string) {
|
||||||
|
value, _ := otto.ToValue(message)
|
||||||
|
panic(value)
|
||||||
|
}
|
@@ -638,8 +638,21 @@ func generateLuaCommandInfo(path string) (*lua.LState, string, []string, string,
|
|||||||
return L, strings.ToLower(cn.String()), categories, d.String(), synchronize, commandType, nil
|
return L, strings.ToLower(cn.String()), categories, d.String(), synchronize, commandType, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (server *SugarDB) buildLuaKeyExtractionFunc(vm any, cmd []string, args []string) (internal.KeyExtractionFuncResult, error) {
|
// luaKeyExtractionFunc executes the extraction function defined in the script and returns the result or error.
|
||||||
L := vm.(*lua.LState)
|
func (server *SugarDB) luaKeyExtractionFunc(cmd []string, args []string) (internal.KeyExtractionFuncResult, error) {
|
||||||
|
// Lock the script before executing the key extraction function
|
||||||
|
script, ok := server.scriptVMs.Load(strings.ToLower(cmd[0]))
|
||||||
|
if !ok {
|
||||||
|
return internal.KeyExtractionFuncResult{}, fmt.Errorf("no lock found for script command %s", cmd[0])
|
||||||
|
}
|
||||||
|
machine := script.(struct {
|
||||||
|
vm any
|
||||||
|
lock *sync.Mutex
|
||||||
|
})
|
||||||
|
machine.lock.Lock()
|
||||||
|
defer machine.lock.Unlock()
|
||||||
|
|
||||||
|
L := machine.vm.(*lua.LState)
|
||||||
// Create command table to pass to the Lua function
|
// Create command table to pass to the Lua function
|
||||||
command := L.NewTable()
|
command := L.NewTable()
|
||||||
for i, s := range cmd {
|
for i, s := range cmd {
|
||||||
@@ -650,17 +663,7 @@ func (server *SugarDB) buildLuaKeyExtractionFunc(vm any, cmd []string, args []st
|
|||||||
for i, s := range args {
|
for i, s := range args {
|
||||||
funcArgs.RawSetInt(i+1, lua.LString(s))
|
funcArgs.RawSetInt(i+1, lua.LString(s))
|
||||||
}
|
}
|
||||||
// Lock the script before executing the key extraction function
|
|
||||||
script, ok := server.scriptVMs.Load(strings.ToLower(cmd[0]))
|
|
||||||
if !ok {
|
|
||||||
return internal.KeyExtractionFuncResult{}, fmt.Errorf("no lock found for script command %s", command)
|
|
||||||
}
|
|
||||||
machine := script.(struct {
|
|
||||||
vm any
|
|
||||||
lock *sync.Mutex
|
|
||||||
})
|
|
||||||
machine.lock.Lock()
|
|
||||||
defer machine.lock.Unlock()
|
|
||||||
// Call the Lua key extraction function
|
// Call the Lua key extraction function
|
||||||
var err error
|
var err error
|
||||||
_ = L.CallByParam(lua.P{
|
_ = L.CallByParam(lua.P{
|
||||||
@@ -706,8 +709,21 @@ func (server *SugarDB) buildLuaKeyExtractionFunc(vm any, cmd []string, args []st
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (server *SugarDB) buildLuaHandlerFunc(vm any, command string, args []string, params internal.HandlerFuncParams) ([]byte, error) {
|
// luaHandlerFunc executes the extraction function defined in the script nad returns the RESP response or error.
|
||||||
L := vm.(*lua.LState)
|
func (server *SugarDB) luaHandlerFunc(command string, args []string, params internal.HandlerFuncParams) ([]byte, error) {
|
||||||
|
// Lock this script's execution key before executing the handler.
|
||||||
|
script, ok := server.scriptVMs.Load(command)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("no lock found for script command %s", command)
|
||||||
|
}
|
||||||
|
machine := script.(struct {
|
||||||
|
vm any
|
||||||
|
lock *sync.Mutex
|
||||||
|
})
|
||||||
|
machine.lock.Lock()
|
||||||
|
defer machine.lock.Unlock()
|
||||||
|
|
||||||
|
L := machine.vm.(*lua.LState)
|
||||||
// Lua table context
|
// Lua table context
|
||||||
ctx := L.NewTable()
|
ctx := L.NewTable()
|
||||||
ctx.RawSetString("protocol", lua.LNumber(params.Context.Value("Protocol").(int)))
|
ctx.RawSetString("protocol", lua.LNumber(params.Context.Value("Protocol").(int)))
|
||||||
@@ -786,17 +802,7 @@ func (server *SugarDB) buildLuaHandlerFunc(vm any, command string, args []string
|
|||||||
for i, s := range args {
|
for i, s := range args {
|
||||||
funcArgs.RawSetInt(i+1, lua.LString(s))
|
funcArgs.RawSetInt(i+1, lua.LString(s))
|
||||||
}
|
}
|
||||||
// Lock this script's execution key before executing the handler
|
|
||||||
script, ok := server.scriptVMs.Load(command)
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("no lock found for script command %s", command)
|
|
||||||
}
|
|
||||||
lock := script.(struct {
|
|
||||||
vm any
|
|
||||||
lock *sync.Mutex
|
|
||||||
})
|
|
||||||
lock.lock.Lock()
|
|
||||||
defer lock.lock.Unlock()
|
|
||||||
// Call the lua handler function
|
// Call the lua handler function
|
||||||
var err error
|
var err error
|
||||||
_ = L.CallByParam(lua.P{
|
_ = L.CallByParam(lua.P{
|
||||||
|
@@ -105,7 +105,7 @@ type SugarDB struct {
|
|||||||
|
|
||||||
commandsRWMut sync.RWMutex // Mutex used for modifying/reading the list of commands in the instance.
|
commandsRWMut sync.RWMutex // Mutex used for modifying/reading the list of commands in the instance.
|
||||||
commands []internal.Command // Holds the list of all commands supported by SugarDB.
|
commands []internal.Command // Holds the list of all commands supported by SugarDB.
|
||||||
// Each commands that's added using a script (e.g. lua), will have a lock associated with the command.
|
// Each commands that's added using a script (lua,js), will have a lock associated with the command.
|
||||||
// Only one goroutine will be able to trigger a script-associated command at a time. This is because the VM state
|
// Only one goroutine will be able to trigger a script-associated command at a time. This is because the VM state
|
||||||
// for each of the commands is not thread safe.
|
// for each of the commands is not thread safe.
|
||||||
// This map's shape is map[string]struct{vm: any, lock: sync.Mutex} with the string key being the command name.
|
// This map's shape is map[string]struct{vm: any, lock: sync.Mutex} with the string key being the command name.
|
||||||
@@ -639,7 +639,7 @@ func (server *SugarDB) ShutDown() {
|
|||||||
log.Println("shutting down script vms...")
|
log.Println("shutting down script vms...")
|
||||||
server.commandsRWMut.Lock()
|
server.commandsRWMut.Lock()
|
||||||
for _, command := range server.commands {
|
for _, command := range server.commands {
|
||||||
if slices.Contains([]string{"LUA_SCRIPT"}, command.Type) {
|
if slices.Contains([]string{"LUA_SCRIPT", "JS_SCRIPT"}, command.Type) {
|
||||||
v, ok := server.scriptVMs.Load(command.Command)
|
v, ok := server.scriptVMs.Load(command.Command)
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
|
Reference in New Issue
Block a user