2020-01-21 19:19:18 +00:00
|
|
|
// Copyright IBM Corp. 2016,2019. All Rights Reserved.
|
2016-11-15 21:46:23 +00:00
|
|
|
// Node module: loopback
|
|
|
|
// This file is licensed under the MIT License.
|
|
|
|
// License text available at https://opensource.org/licenses/MIT
|
|
|
|
|
|
|
|
'use strict';
|
2019-10-07 09:45:34 +00:00
|
|
|
const g = require('../../lib/globalize');
|
2016-08-08 15:24:17 +00:00
|
|
|
|
2016-09-13 23:59:33 +00:00
|
|
|
/**
|
|
|
|
* Data model for key-value databases.
|
|
|
|
*
|
|
|
|
* @class KeyValueModel
|
|
|
|
* @inherits {Model}
|
|
|
|
*/
|
|
|
|
|
2016-08-08 15:24:17 +00:00
|
|
|
module.exports = function(KeyValueModel) {
|
2016-09-13 23:59:33 +00:00
|
|
|
/**
|
|
|
|
* Return the value associated with a given key.
|
|
|
|
*
|
|
|
|
* @param {String} key Key to use when searching the database.
|
|
|
|
* @options {Object} options
|
|
|
|
* @callback {Function} callback
|
|
|
|
* @param {Error} err Error object.
|
|
|
|
* @param {Any} result Value associated with the given key.
|
|
|
|
* @promise
|
|
|
|
*
|
|
|
|
* @header KeyValueModel.get(key, cb)
|
|
|
|
*/
|
2016-08-08 15:24:17 +00:00
|
|
|
KeyValueModel.get = function(key, options, callback) {
|
|
|
|
throwNotAttached(this.modelName, 'get');
|
|
|
|
};
|
|
|
|
|
2016-09-13 23:59:33 +00:00
|
|
|
/**
|
|
|
|
* Persist a value and associate it with the given key.
|
|
|
|
*
|
|
|
|
* @param {String} key Key to associate with the given value.
|
|
|
|
* @param {Any} value Value to persist.
|
|
|
|
* @options {Number|Object} options Optional settings for the key-value
|
|
|
|
* pair. If a Number is provided, it is set as the TTL (time to live) in ms
|
|
|
|
* (milliseconds) for the key-value pair.
|
|
|
|
* @property {Number} ttl TTL for the key-value pair in ms.
|
|
|
|
* @callback {Function} callback
|
|
|
|
* @param {Error} err Error object.
|
|
|
|
* @promise
|
|
|
|
*
|
|
|
|
* @header KeyValueModel.set(key, value, cb)
|
|
|
|
*/
|
2016-08-08 15:24:17 +00:00
|
|
|
KeyValueModel.set = function(key, value, options, callback) {
|
|
|
|
throwNotAttached(this.modelName, 'set');
|
|
|
|
};
|
|
|
|
|
2016-09-13 23:59:33 +00:00
|
|
|
/**
|
|
|
|
* Set the TTL (time to live) in ms (milliseconds) for a given key. TTL is the
|
|
|
|
* remaining time before a key-value pair is discarded from the database.
|
|
|
|
*
|
|
|
|
* @param {String} key Key to use when searching the database.
|
|
|
|
* @param {Number} ttl TTL in ms to set for the key.
|
|
|
|
* @options {Object} options
|
|
|
|
* @callback {Function} callback
|
|
|
|
* @param {Error} err Error object.
|
|
|
|
* @promise
|
|
|
|
*
|
|
|
|
* @header KeyValueModel.expire(key, ttl, cb)
|
|
|
|
*/
|
2016-08-08 15:24:17 +00:00
|
|
|
KeyValueModel.expire = function(key, ttl, options, callback) {
|
|
|
|
throwNotAttached(this.modelName, 'expire');
|
|
|
|
};
|
|
|
|
|
2016-09-13 23:59:33 +00:00
|
|
|
/**
|
|
|
|
* Return the TTL (time to live) for a given key. TTL is the remaining time
|
|
|
|
* before a key-value pair is discarded from the database.
|
|
|
|
*
|
|
|
|
* @param {String} key Key to use when searching the database.
|
|
|
|
* @options {Object} options
|
|
|
|
* @callback {Function} callback
|
|
|
|
* @param {Error} error
|
|
|
|
* @param {Number} ttl Expiration time for the key-value pair. `undefined` if
|
|
|
|
* TTL was not initially set.
|
|
|
|
* @promise
|
|
|
|
*
|
|
|
|
* @header KeyValueModel.ttl(key, cb)
|
|
|
|
*/
|
2016-08-29 21:41:21 +00:00
|
|
|
KeyValueModel.ttl = function(key, options, callback) {
|
|
|
|
throwNotAttached(this.modelName, 'ttl');
|
|
|
|
};
|
|
|
|
|
2016-09-13 23:59:33 +00:00
|
|
|
/**
|
|
|
|
* Return all keys in the database.
|
|
|
|
*
|
|
|
|
* **WARNING**: This method is not suitable for large data sets as all
|
|
|
|
* key-values pairs are loaded into memory at once. For large data sets,
|
|
|
|
* use `iterateKeys()` instead.
|
|
|
|
*
|
|
|
|
* @param {Object} filter An optional filter object with the following
|
|
|
|
* @param {String} filter.match Glob string used to filter returned
|
|
|
|
* keys (i.e. `userid.*`). All connectors are required to support `*` and
|
|
|
|
* `?`, but may also support additional special characters specific to the
|
|
|
|
* database.
|
|
|
|
* @param {Object} options
|
|
|
|
* @callback {Function} callback
|
|
|
|
* @promise
|
|
|
|
*
|
|
|
|
* @header KeyValueModel.keys(filter, cb)
|
|
|
|
*/
|
2016-08-16 12:56:15 +00:00
|
|
|
KeyValueModel.keys = function(filter, options, callback) {
|
|
|
|
throwNotAttached(this.modelName, 'keys');
|
|
|
|
};
|
|
|
|
|
2016-09-13 23:59:33 +00:00
|
|
|
/**
|
|
|
|
* Asynchronously iterate all keys in the database. Similar to `.keys()` but
|
|
|
|
* instead allows for iteration over large data sets without having to load
|
|
|
|
* everything into memory at once.
|
|
|
|
*
|
|
|
|
* Callback example:
|
|
|
|
* ```js
|
|
|
|
* // Given a model named `Color` with two keys `red` and `blue`
|
|
|
|
* var iterator = Color.iterateKeys();
|
|
|
|
* it.next(function(err, key) {
|
|
|
|
* // key contains `red`
|
|
|
|
* it.next(function(err, key) {
|
|
|
|
* // key contains `blue`
|
|
|
|
* });
|
|
|
|
* });
|
|
|
|
* ```
|
|
|
|
*
|
|
|
|
* Promise example:
|
|
|
|
* ```js
|
|
|
|
* // Given a model named `Color` with two keys `red` and `blue`
|
|
|
|
* var iterator = Color.iterateKeys();
|
|
|
|
* Promise.resolve().then(function() {
|
|
|
|
* return it.next();
|
|
|
|
* })
|
|
|
|
* .then(function(key) {
|
|
|
|
* // key contains `red`
|
|
|
|
* return it.next();
|
|
|
|
* });
|
|
|
|
* .then(function(key) {
|
|
|
|
* // key contains `blue`
|
|
|
|
* });
|
|
|
|
* ```
|
|
|
|
*
|
|
|
|
* @param {Object} filter An optional filter object with the following
|
|
|
|
* @param {String} filter.match Glob string to use to filter returned
|
|
|
|
* keys (i.e. `userid.*`). All connectors are required to support `*` and
|
|
|
|
* `?`. They may also support additional special characters that are
|
|
|
|
* specific to the backing database.
|
|
|
|
* @param {Object} options
|
|
|
|
* @returns {AsyncIterator} An Object implementing `next(cb) -> Promise`
|
|
|
|
* function that can be used to iterate all keys.
|
|
|
|
*
|
|
|
|
* @header KeyValueModel.iterateKeys(filter)
|
|
|
|
*/
|
2016-08-16 12:56:15 +00:00
|
|
|
KeyValueModel.iterateKeys = function(filter, options) {
|
|
|
|
throwNotAttached(this.modelName, 'iterateKeys');
|
|
|
|
};
|
|
|
|
|
2016-09-13 23:59:33 +00:00
|
|
|
/*!
|
|
|
|
* Set up remoting metadata for this model.
|
|
|
|
*
|
|
|
|
* **Notes**:
|
|
|
|
* - The method is called automatically by `Model.extend` and/or
|
|
|
|
* `app.registry.createModel`
|
|
|
|
* - In general, base models use call this to ensure remote methods are
|
|
|
|
* inherited correctly, see bug at
|
|
|
|
* https://github.com/strongloop/loopback/issues/2350
|
|
|
|
*/
|
2016-08-08 15:24:17 +00:00
|
|
|
KeyValueModel.setup = function() {
|
|
|
|
KeyValueModel.base.setup.apply(this, arguments);
|
|
|
|
|
|
|
|
this.remoteMethod('get', {
|
|
|
|
accepts: {
|
|
|
|
arg: 'key', type: 'string', required: true,
|
2016-11-15 21:46:23 +00:00
|
|
|
http: {source: 'path'},
|
2016-08-08 15:24:17 +00:00
|
|
|
},
|
2016-11-15 21:46:23 +00:00
|
|
|
returns: {arg: 'value', type: 'any', root: true},
|
|
|
|
http: {path: '/:key', verb: 'get'},
|
|
|
|
rest: {after: convertNullToNotFoundError},
|
2016-08-08 15:24:17 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
this.remoteMethod('set', {
|
|
|
|
accepts: [
|
2016-11-15 21:46:23 +00:00
|
|
|
{arg: 'key', type: 'string', required: true,
|
|
|
|
http: {source: 'path'}},
|
|
|
|
{arg: 'value', type: 'any', required: true,
|
|
|
|
http: {source: 'body'}},
|
|
|
|
{arg: 'ttl', type: 'number',
|
|
|
|
http: {source: 'query'},
|
|
|
|
description: 'time to live in milliseconds'},
|
2016-08-08 15:24:17 +00:00
|
|
|
],
|
2016-11-15 21:46:23 +00:00
|
|
|
http: {path: '/:key', verb: 'put'},
|
2016-08-08 15:24:17 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
this.remoteMethod('expire', {
|
|
|
|
accepts: [
|
2016-11-15 21:46:23 +00:00
|
|
|
{arg: 'key', type: 'string', required: true,
|
|
|
|
http: {source: 'path'}},
|
|
|
|
{arg: 'ttl', type: 'number', required: true,
|
|
|
|
http: {source: 'form'}},
|
2016-08-08 15:24:17 +00:00
|
|
|
],
|
2016-11-15 21:46:23 +00:00
|
|
|
http: {path: '/:key/expire', verb: 'put'},
|
2016-08-08 15:24:17 +00:00
|
|
|
});
|
2016-08-16 12:56:15 +00:00
|
|
|
|
2016-08-29 21:41:21 +00:00
|
|
|
this.remoteMethod('ttl', {
|
|
|
|
accepts: {
|
|
|
|
arg: 'key', type: 'string', required: true,
|
2016-11-15 21:46:23 +00:00
|
|
|
http: {source: 'path'},
|
2016-08-29 21:41:21 +00:00
|
|
|
},
|
2016-11-15 21:46:23 +00:00
|
|
|
returns: {arg: 'value', type: 'any', root: true},
|
|
|
|
http: {path: '/:key/ttl', verb: 'get'},
|
2016-08-29 21:41:21 +00:00
|
|
|
});
|
|
|
|
|
2016-08-16 12:56:15 +00:00
|
|
|
this.remoteMethod('keys', {
|
|
|
|
accepts: {
|
|
|
|
arg: 'filter', type: 'object', required: false,
|
2016-11-15 21:46:23 +00:00
|
|
|
http: {source: 'query'},
|
2016-08-16 12:56:15 +00:00
|
|
|
},
|
2016-11-15 21:46:23 +00:00
|
|
|
returns: {arg: 'keys', type: ['string'], root: true},
|
|
|
|
http: {path: '/keys', verb: 'get'},
|
2016-08-16 12:56:15 +00:00
|
|
|
});
|
2016-08-08 15:24:17 +00:00
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
function throwNotAttached(modelName, methodName) {
|
|
|
|
throw new Error(g.f(
|
|
|
|
'Cannot call %s.%s(). ' +
|
2017-12-12 08:33:15 +00:00
|
|
|
'The %s method has not been setup. ' +
|
2016-08-08 15:24:17 +00:00
|
|
|
'The {{KeyValueModel}} has not been correctly attached ' +
|
|
|
|
'to a {{DataSource}}!',
|
2019-11-17 19:04:57 +00:00
|
|
|
modelName, methodName, methodName,
|
2018-08-08 15:22:20 +00:00
|
|
|
));
|
2016-08-08 15:24:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function convertNullToNotFoundError(ctx, cb) {
|
|
|
|
if (ctx.result !== null) return cb();
|
|
|
|
|
2019-10-07 09:45:34 +00:00
|
|
|
const modelName = ctx.method.sharedClass.name;
|
|
|
|
const id = ctx.getArgByName('id');
|
|
|
|
const msg = g.f('Unknown "%s" {{key}} "%s".', modelName, id);
|
|
|
|
const error = new Error(msg);
|
2016-08-08 15:24:17 +00:00
|
|
|
error.statusCode = error.status = 404;
|
|
|
|
error.code = 'KEY_NOT_FOUND';
|
|
|
|
cb(error);
|
|
|
|
}
|