Gathering detailed insights and metrics for loopback-connector-cloudant
Gathering detailed insights and metrics for loopback-connector-cloudant
Gathering detailed insights and metrics for loopback-connector-cloudant
Gathering detailed insights and metrics for loopback-connector-cloudant
npm install loopback-connector-cloudant
Module System
Min. Node Version
Typescript Support
Node Version
NPM Version
19 Stars
265 Commits
21 Forks
33 Watching
19 Branches
26 Contributors
Updated on 02 Nov 2023
JavaScript (92.74%)
Shell (7.26%)
Cumulative downloads
Total Downloads
Last day
-18.3%
237
Compared to previous day
Last week
-63.2%
692
Compared to previous week
Last month
50.9%
7,629
Compared to previous month
Last year
15.8%
74,423
Compared to previous year
IBM Cloudant® is a NoSQL database platform built for the cloud. You can use Cloudant as a fully-managed DBaaS running on public cloud platforms like Bluemix and SoftLayer or via an on-premises version called Cloudant Local.
For more information, see Getting started with Cloudant NoSQL DB
The loopback-connector-cloudant
module is the Cloudant connector for the LoopBack framework.
LoopBack tries best to fit its model to a specific database's design, while limited by the nature of database, it's not always possible to support all LoopBack features perfectly, and user should be aware of some key features about Cloudant before they start to design a Cloudant model.
Cloudant does not support the idea of updating a document. All "updates" on a document are destructive replacements.
It implies that if you do want to partially update a document, please make sure unchanged values are included in the update object.
For example:
// original document
{
"id": ...,
"_rev": ...,
"prop1": "1",
"prop2": "2",
}
// data to be updated
ds.updateOrCreate('User', {
prop1: 'updated1',
}, function (err, res) {});
// document after update
{
"id": ...,
"_rev": ...,
"prop1": "updated1",
}
Please note how property prop2
was completely dropped upon update.
We have some discussion on update methods, the issue link can be found in Feature Backlog section
Cloudant is not designed to change same document frequently and multiple times. It stores status changes by creating different documents and including the same unique id to tell that they are attached to the same item, not updating the same document.
By modeling the data in separate documents that are only written once, we can reduce the chance of concurrent access to the same document by separate processes.
And by properly controlling the conflict, developer can still do a safe modify. For details, refer to Conflict Control
The basic idea is when modifying a document, user needs to control conflict by handling the revision of a document, currently the connector controls this process, after retriving the latest revision, connector uses it to update/delete doc, and returns 409 conflict error if doc changes during that time slot. In the middle, user could not interfere and provide their own conflict solution.
Unlike relational db or mongodb, Cloudant doesn't have a concept as 'table' or 'collection', data in a Cloudant database are all stored as documents.
The connector uses a design document to represent a LoopBack model, and common documents to represent model instances.
The following is a comparison among different databases:
Model | Model Property | Model Instance | |
---|---|---|---|
Relational DB | table | column in table | row in table |
Mongodb | collection | createIndex if property.index is true | document in collection |
Cloudant | Design documents in database | NOT stored in document | common documents in database |
To create a model, the connector creates a design document with the following config:
type: 'text',
name: 'lb-index-' + modelName,
ddoc: 'lb-index-ddoc-' + modelName,
index: {
default_field: {
enabled: false,
},
selector: {
[modelIndex]: modelName
},
},
By default, modelIndex
is 'loopback__model__name', and modelSelector
is {[modelIndex]: modelName}. User can customize modelSelector
and modelIndex
in datasource's json file, for details please check model-specific configuration
To create a model instance, the connector creates a non-design document with value of property 'loopback__model__name' equals to modelName
.
For model properties, we plan to create index for property that has config index: true
. In the future, it will be the same way as what mongodb connector does.
You can specify configurations per model for database selection and to map a model to a different document:
common/models/model-name.json
{
"name": "User",
"base": "PersistedModel",
"idInjection": true,
...
"cloudant": {
"modelIndex": "custom_doc_type_property_name",
"modelSelector": { "doc_type": "user" },
"database": "test2"
},
...
Model-specific configuration settings:
Property       | Type | Description |
---|---|---|
database | String | Database name |
modelIndex | String | Specify the model name to document mapping, defaults to loopback__model__name . |
modelSelector | JSON | Use the Cloudant Query selector syntax to associate models to existing data. NOTE: modelSelector and modelIndex are mutually exclusive; see Selector syntax. |
In a document, property _rev
is the latest doc revision and must be provided when modifying the doc.
Our connector allows the user to retrieve back the _rev
property upon all CRUD operations, however does not add it to the model definition.
If you would like to have a _rev
property on your model, as an end user, the onus is on you to add the property in the model definition.
Note: All CRUD operations require _rev
(except create) and it is up to the user to specify them. The connector does not handle such cases due to possibilities of race condition when two users try to update the same document.
_rev
model.json
1{ 2 ... 3 "properties": { 4 "_rev": { 5 "type": "string" 6 }, 7 "name": { 8 "type": "string" 9 } 10 }, 11 ... 12}
1 Model.create([{ 2 name: 'Foo', 3 }, { 4 name: 'Bar', 5 }], function(err, result) { 6 if (err) throw err; 7 console.log('Created instance: ' + JSON.stringify(result)); 8 });
Note: Cloudant does not allow customized _rev
value, hence creating an instance with a _rev
value will not give the expected result (i.e Cloudant's CREATE operation ignores the _rev
value when provided and generates a random unique one). The onus is on the user if they fail to comply to this rule.
Let's say we have an instance in the database:
1{ 2 "id":"2", 3 "_rev":"2-abcedf", 4 "name":"Bar" 5}
Find
1 Model.find(function(err, result) { 2 if (err) throw err; 3 console.log('Found all instances: ' + JSON.stringify(result)); 4 });
1 Model.findById('2', function(err, result) { 2 if (err) throw err; 3 console.log('Found instance with id: ' + JSON.stringify(result)); 4 });
Replace
1 Model.replaceOrCreate({ 2 id:'2', 3 _rev:'2-abcedf', 4 name:'Bar2' 5 }, function(err, result) { 6 if (err) throw err; 7 console.log('Replace an existing instance: ' + JSON.stringify(result)); 8 });
1 Model.replaceById('2', { 2 _rev:'2-abcedf', 3 name:'Bar3' 4 }, function(err, result) { 5 if (err) throw err; 6 console.log('Replace an existing instance with id: ' + JSON.stringify(result)); 7 });
Update
1 Model.updateOrCreate({ 2 id:'2', 3 _rev:'2-abcedf', 4 name:'Bar4' 5 }, function(err, result) { 6 if (err) throw err; 7 console.log('Update an existing instance: ' + JSON.stringify(result)); 8 });
update/updateAll
with _rev
property
1 Model.updateAll({ 2 _rev:'2-abcedf', 3 name:'Bar4' 4 }, {name: 'Bar4-updated', _rev: '2-abcedf'}, function(err, result) { 5 if (err) throw err; 6 console.log('Update an existing instance: ' + JSON.stringify(result)); 7 });
without _rev
property
1 Model.updateAll({ 2 name:'Bar4' 3 }, {name: 'Bar4-updated'}, function(err, result) { 4 if (err) throw err; 5 console.log('Update an existing instance: ' + JSON.stringify(result)); 6 });
In loopback forceId
means user can specify the value of the primary key when creating a model instance, instead of using an auto-generated one. Learn more about LoopBack's forceId.
We recommend user to be careful when creating customized model id instead of using the auto-generated one because data belonging to different models can interfere with each other.
Every document stored in Cloudant has an unique id, stored as _id
, and the database has built-in index for it. Retrieving data by its _id
gives a better performance than querying by other field. Therefore, the connector always assign the _id
field as the loopback model's primary key.
If you have two models that allow customized _id
as primary key set with "forceId": false
in the model definition, it could result in a document update conflict.
For example:
{_id: 'myid', loopback__model__name: 'Foo'}
{_id: 'myid', loopback__model__name: 'Bar'}
Here is an example of a case that would result in a document update conflict using "forceId": false
:
1{ 2 "name": "Employee", 3 "base": "PersistedModel", 4 "idInjection": true, 5 "forceId": false, 6 "options": { 7 "validateUpsert": true 8 }, 9 "properties": { 10 "_id": { 11 "type": "number", 12 "id": true, 13 "required": true 14 }, 15 "name": { 16 "type": "string" 17 }, 18 "age": { 19 "type": "number" 20 } 21 }, 22 "validations": [], 23 "relations": {}, 24 "acls": [], 25 "methods": {} 26} 27
1{ 2 "name": "Car", 3 "base": "PersistedModel", 4 "idInjection": true, 5 "forceId": false, 6 "options": { 7 "validateUpsert": true 8 }, 9 "properties": { 10 "_id": { 11 "type": "number", 12 "id": true, 13 "required": true 14 }, 15 "make": { 16 "type": "string" 17 }, 18 "model": { 19 "type": "string" 20 } 21 }, 22 "validations": [], 23 "relations": {}, 24 "acls": [], 25 "methods": {} 26}
1'use strict'; 2 3var util = require('util'); 4var _ = require('lodash'); 5 6module.exports = function(app) { 7 var db = app.datasources.cloudantDs; 8 var Employee = app.models.Employee; 9 var Car = app.models.Car; 10 11 db.once('connected', function() { 12 db.automigrate(function(err) { 13 if (err) throw err; 14 console.log('\nAutomigrate completed'); 15 16 Employee.create([{ 17 _id: 1, 18 name: 'Foo', 19 }, { 20 _id: 2, 21 name: 'Bar', 22 }], function(err, result) { 23 if (err) throw err; 24 console.log('\nCreated employee instance: ' + util.inspect(result)); 25 26 Car.create([{ 27 _id: 1, 28 make: 'Toyota', 29 }, { 30 _id: 2, 31 name: 'BMW', 32 }], function(err, result) { 33 if (err) throw err; 34 console.log('\nCreated car instance: ' + util.inspect(result)); 35 }); 36 }); 37 }); 38 }); 39};
Running the above script will throw a document update conflict because there exists a model instance with _id
value of 1 and 2.
1Web server listening at: http://localhost:3000 2Browse your REST API at http://localhost:3000/explorer 3 4Automigrate completed 5Created employee instance: [ { _id: '1', name: 'Foo' }, { _id: '2', name: 'Bar' } ] 6 7/Users/ssh/workspace/sandbox/loopback-sandbox/apps/cloudant/cloudant-forceId-app/server/boot/script.js:33 8 if (err) throw err; 9 ^ 10Error: Document update conflict. (duplicate?),Error: Document update conflict. (duplicate?)
In order to avoid this pitfall, please set "forceId": true
on either of the model definition which would allow one of the models to have an auto-generated id or do not set "forceId": false
on either of the model definitions.
For user that don't have a cloudant server to develop or test, here are some suggestions can help you quickly setup one.
For development use, a docker container of Cloudant local is easy to setup and there is no request limit per second.
Bluemix Cloudant will be more stable for production.
Cloudant local (docker image)
Cloudant on Bluemix
Limit request per second by default.
Choose Bluemix Cloudant if you already have a Bluemix account with a better situation than limited-days' free trial.
Setup steps:
Cloudant DBaaS account
To view the Cloudant dashboard on both DBaaS and Bluemix, sign in with your Cloudant username and password.
Enter the following in the top-level directory of your LoopBack application:
$ npm install loopback-connector-cloudant --save
The --save
option adds the dependency to the application’s package.json
file.
Use the Data source generator to add the Cloudant data source to your
application. The entry in the applications /server/datasources.json
will
look something like this:
"mydb": {
"name": "mydb",
"connector": "cloudant",
"url": "https://<username>:<password>@<host>"
"database": "test"
}
or
"mydb": {
"name": "mydb",
"connector": "cloudant",
"username": "XXXX-bluemix",
"password": "YYYYYYYYYYYY",
"database": "test"
}
url
vs username
& password
NOTE: The url
property will override username
and password
.
It means only when missing url
, cloudant will use username
and password
to create connection.
If url
is wrong, even user provide correct username
and password
, the connection still fails.
Only specify the username
and password
fields if you are using the root/admin user for the Cloudant server which has the same
string as the hostname for the Cloudant server, because the Cloudant driver used by the connector appends .cloudant.com
to the
username
field when username
and password
fields are specified. Therefore, it is good practice to use the url
field instead
of username
and password
if your host is not username.cloudant.com
.
Edit datasources.json
to add other supported properties as required:
Property | Type | Description |
---|---|---|
database | String | Database name |
username | String | Cloudant username, use either 'url' or username/password |
password | String | Cloudant password |
url | String | Cloudant URL containing both username and password |
modelIndex | String | Specify the model name to document mapping, defaults to loopback__model__name |
We honor the plugins’ configuration and passes them to the driver. Cloudant plugins has section 'iamauth' to show how the IAM authentication is handled by the driver.
To connect to the Cloudant database using IAM authentication, you can configure your LoopBack datasource as:
1"mydb": { 2 "name": "mydb", 3 "connector": "cloudant", 4 // make sure you provide the password here 5 "url": "https://<username>:<password>@<host>" 6 "database": "test", 7 "plugins": { 8 "iamauth": { "iamApiKey": "xxxxxxxxxx"} 9 } 10}
Besides the basic configuration properties, user can provide advanced configuration information, for example proxy, in requestDefaults
:
"mydb": {
"name": "mydb",
"connector": "cloudant",
"url": "https://<username>:<password>@<host>"
"database": "test",
"requestDefaults": {"proxy": "http://localhost:8080"}
}
For details, refer to the driver(nodejs-cloudant) document
User can provide plugin name and parameters in datasource object. For example, connect to a Cloudant server with plugin called retry
, with parameters retryAttempts
and retryTimeout
:
"mydb": {
"name": "mydb",
"connector": "cloudant",
"url": "https://<username>:<password>@<host>"
"database": "test",
"plugin": "retry",
"retryAttempts": 5,
"retryTimeout": 1000
}
Please note user can only use one of the plugins list in Cloudant driver's doc, not multiple: https://github.com/cloudant/nodejs-cloudant#request-plugins
1// Inside file /server/script.js 2var util = require('util'); 3 4// Here we create datasource dynamically. 5// If you already define a static datasource in server/datasources.json, 6// please check how to load it in 7// https://github.com/cloudant/nodejs-cloudant#example-code 8var DataSource = require ('loopback-datasource-juggler').DataSource, 9 Cloudant = require ('loopback-connector-cloudant'); 10 11var config = { 12 username: 'your_cloudant_username', 13 password: 'your_cloudant_password', 14 database: 'your_cloudant_database' 15}; 16 17var db = new DataSource (Cloudant, config); 18 19Test = db.define ('Test', { 20 name: { type: String }, 21}); 22 23// wait for connected event on the 24// datasource before doing any database 25// operations since we connect asynchronously 26db.once('connected', function() { 27 Test.create({ 28 name: "Tony", 29 }).then(function(test) { 30 console.log('create instance ' + util.inspect(test, 4)); 31 return Test.find({ where: { name: "Tony" }}); 32 }).then(function(test) { 33 console.log('find instance: ' + util.inspect(test, 4)); 34 return Test.destroyAll(); 35 }).then(function(test) { 36 console.log('destroy instance!'); 37 }).catch(err); 38});
User can find most CRUD operation apis documented in https://loopback.io/doc/en/lb3/Built-in-models-REST-API.html
We are still in progress of refactoring some methods, more details to be updated.
Currently update
does the same thing as replace
, for details, refer to https://github.com/loopbackio/loopback-connector-cloudant#no-partial-update
After attaching a model to a Cloudant datasource, either statically with model.json
file or dynamically in boot script code, user need to run automigrate
or autoupdate
to migrate models to database. Cloudant connector does NOT automatically migrate them.
The following migration functions take either an array of multiple model's name, or a string of a single model's name. The example code will show how to do it.
autoupdate
does not destroy existing model instances if model already defined in database. It only creates design document for new models.
Under the hood Cloudant allows creating same design doc multiple times, it doesn't return error, but returns existed
as result to tell is it a new design doc or existing one.
automigrate
destroys existing model instances if model already defined in database. Please make sure you do want to clean up data before running automigrate
. Then it does same thing as autoupdate
User can call this function to check if model exists in database.
/server/script.js
1module.export = function migrateData(app) { 2 // Suppose you already define a datasource called `cloudantDS` 3 // in server/datasources.json 4 var ds = app.datasources.cloudantDS; 5 6 // static model created with model.json file 7 var StaticModel = app.models.StaticModel; 8 // dynamic model created in boot script 9 var DynamicModel = ds.define('DynamicModel', { 10 name: {type: String}, 11 description: {type: String}, 12 }); 13 14 // Write the three examples in parallel just to avoid dup code, 15 // please try ONLY ONE of them at one time. 16 ds.once('connected', function() { 17 // try autoupdate example - multiple models 18 ds.autoupdate(['StaticModel', 'DynamicModel'], function(err) {}); 19 // OR 20 // try automigrate example - single model 21 ds.automigrate('StaticModel', function(err) {}); 22 // OR 23 // try isActual example - if any model exist, run autoupdate, otherwise automigrate 24 ds.isActual(['StaticModel', 'DynamicModel'], function(err, exist) { 25 if (exist) { 26 ds.autoupdate(['StaticModel', 'DynamicModel'], function(err){}) 27 } else { 28 ds.automigate(['StaticModel', 'DynamicModel'], function(err){}); 29 } 30 }); 31 }); 32}
Not implemented yet, track it in story https://github.com/loopbackio/loopback-connector-cloudant/issues/118
Given a design doc name and the view name in it, user can use a connector level function viewDocs
to query the view.
Since viewDocs
is a specific api for Cloudant connector only, it is not attached to the dataSource Object defined in loopback-datasource-juggler, which means the correct way to call it is ds.connector.viewDocs
:
/server/script.js
1module.exports = function(server) { 2 // Get Cloudant dataSource as `ds` 3 // 'cloudantDB' is the name of Cloudant datasource created in 4 // 'server/datasources.json' file 5 var ds = server.datasources.cloudantDB; 6 7 ds.once('connected', function() { 8 // 1. Please note `ds.connector.viewDocs()` is the correct way to call it, 9 // NOT `ds.viewDocs()` 10 // 2. This api matches the Cloudant endpoint: 11 // GET /db/_design/<design-doc>/_view/<view-name> 12 ds.connector.viewDocs('design_doc', 'view_name', function(err, results) { 13 // `results` would be the data returned by querying that view 14 }); 15 16 // Alternatively user can also specify the filter for view query 17 ds.connector.viewDocs('design_doc', 'view_name', {key: 'filter'}, 18 function(err, results) {}); 19 }); 20};
Given a design doc name and the filter name in it, user can use a connector level function geoDocs
to query a geospatial index.
Since geoDocs
is a specific api for Cloudant connector only, it is not attached to the dataSource Object defined in loopback-datasource-juggler, which means the correct way to call it is ds.connector.geoDocs
:
/server/script.js
1module.exports = function(server) { 2 // Get Cloudant dataSource as `ds` 3 // 'cloudantDB' is the name of Cloudant datasource created in 4 // 'server/datasources.json' file 5 var ds = server.datasources.cloudantDB; 6 7 ds.once('connected', function() { 8 // 1. Please note `ds.connector.geoDocs()` is the correct way to call it, 9 // NOT `ds.geoDocs()` 10 // 2. This api matches the Cloudant endpoint: 11 // GET /db/_design/<design-doc>/_geo/<index-name> 12 ds.connector.geoDocs('design_doc', 'index_name', function(err, results) { 13 // `results` would be the data returned by querying that geospatial index 14 }); 15 16 // Alternatively user can also specify the filter for geospatial query 17 ds.connector.geoDocs('design_doc', 'index_name', {key: 'filter'}, 18 function(err, results) {}); 19 }); 20};
Given an array of data to be updated, Cloudant supports the idea of performing bulk replace on a model instance. Please note, unlike other CRUD operations, bulk replace does not invoke any operation hooks.
Note: To perform bulk replace, each data in the array data set needs to have the id
and _rev
property corresponding to the documents id
and _rev
property in the database.
Example:
server/boot/script.js
1var dataToCreate = [ 2 {id: 1, name: 'Foo', age: 1}, 3 {id: 2, name: 'Bar', age: 1}, 4 {id: 3, name: 'Baz', age: 2}, 5 {id: 4, name: 'A', age: 4}, 6 {id: 5, name: 'B', age: 5}, 7 {id: 6, name: 'C', age: 6}, 8 {id: 7, name: 'D', age: 7}, 9 {id: 8, name: 'E', age: 8}, 10 ]; 11var dataToUpdate = [ 12 {id: 1, name: 'Foo-change', age: 11}, 13 {id: 5, name: 'B-change', age: 51}, 14 {id: 8, name: 'E-change', age: 91} 15]; 16 17module.exports = function(app) { 18 var db = app.dataSources.cloudantDS; 19 var Employee = app.models.Employee; 20 21 db.once('connected', function() { 22 db.automigrate(function(err) { 23 if (err) throw err; 24 25 Employee.create(dataToCreate, function(err, result) { 26 if (err) throw err; 27 console.log('\nCreated instance: ' + JSON.stringify(result)); 28 29 dataToUpdate[0].id = result[0].id; 30 dataToUpdate[0]._rev = result[0]._rev; 31 dataToUpdate[1].id = result[4].id; 32 dataToUpdate[1]._rev = result[4]._rev; 33 dataToUpdate[2].id = result[7].id; 34 dataToUpdate[2]._rev = result[7]._rev; 35 36 // note: it is called `db.connector.bulkReplace` 37 // rather than `Employee.bulkReplace` 38 db.connector.bulkReplace('Employee', dataToUpdate, function(err, result) { 39 if (err) throw err; 40 41 console.log('\nBulk replace performed: ' + JSON.stringify(result)); 42 43 Employee.find(function(err, result) { 44 if (err) throw err; 45 46 console.log('\nFound all instances: ' + JSON.stringify(result)); 47 }); 48 }); 49 }); 50 }); 51 }); 52};
If you're using partitioned database, see details on how to configure your model and make use of the partitioned search in https://github.com/loopbackio/loopback-connector-cloudant/blob/master/doc/partitioned-db.md
Cloudant local(docker image)
Cloudant DBaaS account
Cloudant on Bluemix
To run the tests:
1CLOUDANT_USERNAME=username CLOUDANT_PASSWORD=password CLOUDANT_HOST=localhost CLOUDANT_HOST=5984 CLOUDANT_DATABASE=database npm run mocha
If you do not have a local Cloudant instance, you can also run the test suite with very minimal requirements. We use couchDB docker image to run the test locally.
NOTICE: we use couchDB3
docker image for testing Cloudant because Cloudant
doesn't have a maintained image, and most of the their functionalities are the
same (except couchDB doesn't support geosearch).
First have Docker installed.
To simply run the tests, the following script would spawn a CouchDB3 instance and run tests automatically on your end:
1npm t
1source setup.sh <HOST> <USER> <PASSWORD> <PORT> <DATABASE>
Where <HOST>
, <PORT>
, <USER>
, <PASSWORD>
and <DATABASE>
are optional parameters. The default values are localhost
, 5984
, admin
, pass
and testdb
respectively. The <USER>
and <PASSWORD>
you set above will be the admin/password of this couchDB3 container.
The script cloudant-config.sh
is generated by the above script. It has all needed environment variables for the tests.
1source cloudant-config.sh && npm run mocha
For more detailed information regarding connector-specific functions and behaviour, see the docs section.
To be updated
No vulnerabilities found.
Reason
security policy file detected
Details
Reason
no binaries found in the repo
Reason
license file detected
Details
Reason
Found 10/18 approved changesets -- score normalized to 5
Reason
0 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0
Reason
no effort to earn an OpenSSF best practices badge detected
Reason
project is not fuzzed
Details
Reason
SAST tool is not run on all commits -- score normalized to 0
Details
Reason
11 existing vulnerabilities detected
Details
Score
Last Scanned on 2024-11-18
The Open Source Security Foundation is a cross-industry collaboration to improve the security of open source software (OSS). The Scorecard provides security health metrics for open source projects.
Learn More