beanbagdb 0.5.50 → 0.5.51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -1
- package/src/index.js +308 -155
- package/src/system_schema.js +30 -37
- package/test/couch_connect.js +29 -0
- package/test/couchdb.js +75 -0
- package/test/init.test.js +1 -1
- package/test/operations.test.js +227 -45
- package/test/pouchdb.js +1 -0
- package/test/test1.js +16 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "beanbagdb",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.51",
|
|
4
4
|
"description": "A JS library to introduce a schema layer to a No-SQL local database",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"module": "src/index.js",
|
|
@@ -34,5 +34,8 @@
|
|
|
34
34
|
"nano": "^10.1.4",
|
|
35
35
|
"pouchdb": "^9.0.0",
|
|
36
36
|
"pouchdb-find": "^9.0.0"
|
|
37
|
+
},
|
|
38
|
+
"mocha": {
|
|
39
|
+
"spec": "test/**/*.test.js"
|
|
37
40
|
}
|
|
38
41
|
}
|
package/src/index.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import * as sys_sch from "./system_schema.js";
|
|
2
|
-
// import { version } from "../package.json" assert {type :"json"};
|
|
3
2
|
/**
|
|
4
3
|
* This the core class. it is not very useful in itself but can be used to generate a sub class for a specific database for eg CouchDB.
|
|
5
4
|
* It takes a db_instance argument, which , this class relies on perform CRUD operations on the data.
|
|
6
5
|
* Why have a "dumb" class ? : So that the core functionalities remains in a single place and the multiple Databases can be supported.
|
|
6
|
+
* Naming convention :
|
|
7
|
+
* - user facing methods : verbs with underscores, no camel case
|
|
8
|
+
* - internal methods (uses the this object) only to be used within the class : name starts with underscore (_)
|
|
9
|
+
* - util methods : these can also be used by the user, this object not accessed, : name starts with util_
|
|
7
10
|
*/
|
|
8
|
-
class BeanBagDB {
|
|
11
|
+
export class BeanBagDB {
|
|
9
12
|
/**
|
|
10
13
|
* @param {object} db_instance - Database object
|
|
11
14
|
* db_instance object contains 3 main keys :
|
|
@@ -16,117 +19,196 @@ class BeanBagDB {
|
|
|
16
19
|
*/
|
|
17
20
|
constructor(db_instance) {
|
|
18
21
|
// data validation checks
|
|
19
|
-
this.
|
|
20
|
-
this.
|
|
21
|
-
this.
|
|
22
|
+
this.util_check_required_fields(["name", "encryption_key", "api", "utils","db_name"],db_instance)
|
|
23
|
+
this.util_check_required_fields(["insert", "update", "delete", "search","get","createIndex"],db_instance.api)
|
|
24
|
+
this.util_check_required_fields(["encrypt", "decrypt","ping","validate_schema"],db_instance.utils)
|
|
22
25
|
|
|
23
|
-
if(db_instance.encryption_key.length
|
|
26
|
+
if(db_instance.encryption_key.length<20){throw new Error("encryption_key must have at least 20 letters")}
|
|
24
27
|
// db name should not be blank,
|
|
25
28
|
|
|
26
|
-
this.name = db_instance.name;
|
|
27
29
|
this.encryption_key = db_instance.encryption_key;
|
|
28
|
-
|
|
30
|
+
this.db_name = db_instance.db_name // couchdb,pouchdb etc...
|
|
29
31
|
this.db_api = db_instance.api;
|
|
30
32
|
this.utils = db_instance.utils;
|
|
31
33
|
|
|
32
|
-
this.
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
this.meta = {
|
|
35
|
+
database_name : db_instance.name,
|
|
36
|
+
backend_database : this.db_name,
|
|
37
|
+
beanbagdb_version_db : null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
this._version = this._get_current_version()
|
|
41
|
+
// latest indicated if the DB was initialized with the latest version or not.
|
|
42
|
+
this.active = false
|
|
35
43
|
|
|
44
|
+
console.log("Run ready() now");
|
|
45
|
+
|
|
36
46
|
this.plugins = {}
|
|
47
|
+
|
|
48
|
+
this.error_codes = {
|
|
49
|
+
not_active : "Database is not ready. Run ready() first",
|
|
50
|
+
schema_not_found:"Schema not found"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async metadata(){
|
|
55
|
+
// returns system data
|
|
56
|
+
return {
|
|
57
|
+
... this.meta,
|
|
58
|
+
beanbagdb_version_code : this._version,
|
|
59
|
+
ready_to_use : this.active
|
|
60
|
+
}
|
|
61
|
+
// todo : doc count, schema count, records for each schema, size of the database,
|
|
37
62
|
}
|
|
38
63
|
|
|
39
64
|
/**
|
|
40
65
|
* This is to check if the database is ready to be used. It it important to run this after the class is initialized.
|
|
41
66
|
*/
|
|
42
67
|
async ready() {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
68
|
+
// TODO Ping db
|
|
69
|
+
let check = { initialized: false, latest: false ,db_version:null};
|
|
70
|
+
let version_search = await this.db_api.search({
|
|
71
|
+
selector: { schema: "system_settings", "data.name": "beanbagdb_version" },
|
|
72
|
+
});
|
|
73
|
+
if (version_search.docs.length > 0) {
|
|
74
|
+
let doc = version_search.docs[0];
|
|
75
|
+
this.active = doc["data"]["value"] == this._version;
|
|
76
|
+
this.meta.beanbagdb_version_db = doc["data"]["value"]
|
|
77
|
+
}
|
|
78
|
+
if(this.active){
|
|
79
|
+
console.log("Ready")
|
|
80
|
+
}else{
|
|
81
|
+
await this.initialize_db()
|
|
49
82
|
}
|
|
50
83
|
}
|
|
51
84
|
|
|
52
|
-
|
|
53
|
-
return this.ready_check.ready
|
|
54
|
-
}
|
|
85
|
+
|
|
55
86
|
|
|
56
87
|
/**
|
|
57
88
|
* Initializes the database making it ready to be used. Typically, required to run after every time package is updated to a new version.
|
|
58
89
|
* See the documentation on the architecture of the DB to understand what default schemas are required for a smooth functioning of the database
|
|
59
90
|
*/
|
|
60
91
|
async initialize_db() {
|
|
92
|
+
// this works on its own but is usually called by ready automatically if required
|
|
93
|
+
|
|
94
|
+
// check for schema_scehma : if yes, check if latest and upgrade if required, if no create a new schema doc
|
|
95
|
+
let logs = ["init started"]
|
|
61
96
|
try {
|
|
62
|
-
|
|
63
|
-
|
|
97
|
+
let schema = await this.get_schema_doc("schema")
|
|
98
|
+
if (schema["data"]["version"] != sys_sch.schema_schema.version){
|
|
99
|
+
logs.push("old schema_schema v "+schema["data"]["version"])
|
|
100
|
+
let full_doc = await this.db_api.get(schema["_id"])
|
|
101
|
+
full_doc["data"] = {...sys_sch.schema_schema}
|
|
102
|
+
full_doc["meta"]["updated_on"] = this._get_now_unix_timestamp()
|
|
103
|
+
await this.db_api.update(full_doc)
|
|
104
|
+
logs.push("new schema_schema v "+sys_sch.schema_schema.version)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
} catch (error) {
|
|
108
|
+
console.log(error)
|
|
109
|
+
if (error.message==this.error_codes.schema_not_found) {
|
|
110
|
+
console.log("...adding new ")
|
|
111
|
+
// inserting new schema_schema doc
|
|
64
112
|
let schema_schema_doc = this._get_blank_doc("schema");
|
|
65
113
|
schema_schema_doc.data = sys_sch.schema_schema;
|
|
66
114
|
await this.db_api.insert(schema_schema_doc);
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
115
|
+
logs.push("init schema_schema v "+sys_sch.schema_schema.version)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
let keys = Object.keys(sys_sch.system_schemas);
|
|
120
|
+
for (let index = 0; index < keys.length; index++) {
|
|
121
|
+
const schema_name = sys_sch.system_schemas[keys[index]]["name"]
|
|
122
|
+
const schema_data = sys_sch.system_schemas[keys[index]];
|
|
123
|
+
try {
|
|
124
|
+
// console.log(schema_name)
|
|
125
|
+
let schema1 = await this.get_schema_doc(schema_name)
|
|
126
|
+
if (schema1["data"]["version"] != schema_data.version){
|
|
127
|
+
logs.push("old "+schema_name+" v "+schema1["data"]["version"])
|
|
128
|
+
let full_doc = await this.db_api.get(schema1["_id"])
|
|
129
|
+
full_doc["data"] = {...schema_data}
|
|
130
|
+
full_doc["meta"]["updated_on"] = this._get_now_unix_timestamp()
|
|
131
|
+
await this.db_api.update(full_doc)
|
|
132
|
+
logs.push("new "+schema_name+" v "+schema_data.version)
|
|
77
133
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
134
|
+
} catch (error) {
|
|
135
|
+
console.log(error)
|
|
136
|
+
if (error.message==this.error_codes.schema_not_found) {
|
|
137
|
+
// inserting new schema doc
|
|
138
|
+
let new_schema_doc = this._get_blank_schema_doc("schema",sys_sch.schema_schema["schema"],schema_data);
|
|
139
|
+
await this.db_api.insert(new_schema_doc);
|
|
140
|
+
logs.push("init "+schema_name+" v "+schema_data.version)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// store the logs in the log_doc , generate it for the first time
|
|
145
|
+
// console.log(logs)
|
|
146
|
+
if(logs.length>1){
|
|
147
|
+
// version needs to be updated in the object as well as settings and must be logged
|
|
148
|
+
logs.push("Init done")
|
|
149
|
+
|
|
150
|
+
await this.insert_or_update_setting("system_logs",{value:{text:logs.join(","),added:this._get_now_unix_timestamp()},"on_update_array":"append"})
|
|
151
|
+
await this.insert_or_update_setting("beanbagdb_version",{value:this._version})
|
|
152
|
+
// await this.insert_or_update_setting("system_logs",{value:{text:"This is just a test.",added:this._get_now_unix_timestamp()}})
|
|
153
|
+
|
|
154
|
+
this.meta.beanbagdb_version_db = this._version
|
|
155
|
+
this.active = true
|
|
156
|
+
}else{
|
|
157
|
+
// no new updates were done
|
|
158
|
+
console.log("already updated. nothing is required to be done. continue")
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
async insert_or_update_setting(name,new_data,schema={}){
|
|
162
|
+
// TODO implement schema check
|
|
163
|
+
if(!new_data){throw new Error("No data provided")}
|
|
164
|
+
if(!new_data.value){throw new Error("No value provided")}
|
|
165
|
+
|
|
166
|
+
let doc_search = await this.db_api.search({"selector":{"schema":"system_settings","data.name":name}})
|
|
167
|
+
if(doc_search.docs.length>0){
|
|
168
|
+
// doc already exists, check schema and update it : if it exists then it's value already exists and can be
|
|
169
|
+
let doc = {...doc_search.docs[0]}
|
|
170
|
+
if(Array.isArray(doc.data.value)){
|
|
171
|
+
let append_type = doc.data.on_update_array
|
|
172
|
+
if(append_type=="append"){
|
|
173
|
+
doc["data"]["value"].push(new_data.value)
|
|
174
|
+
}else if(append_type=="update"){
|
|
175
|
+
doc["data"]["value"] = new_data.value
|
|
176
|
+
}else{
|
|
177
|
+
throw new Error("Invalid on update array value")
|
|
104
178
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
// finally update the flags
|
|
108
|
-
this.ready_check.initialized = true;
|
|
109
|
-
this.ready_check.latest = true;
|
|
110
|
-
console.log("Database initialized");
|
|
111
|
-
} else {
|
|
112
|
-
console.log("Database already initialized");
|
|
113
|
-
if (!this.ready_check.latest) {
|
|
114
|
-
// update to latest schema
|
|
115
|
-
this._update_system_schema();
|
|
116
|
-
} else {
|
|
117
|
-
console.log("Database already up to date");
|
|
179
|
+
}else{
|
|
180
|
+
doc["data"]["value"] = new_data.value
|
|
118
181
|
}
|
|
182
|
+
// finally update it
|
|
183
|
+
doc["meta"]["updated_on"] = this._get_now_unix_timestamp()
|
|
184
|
+
await this.db_api.update(doc)
|
|
185
|
+
return doc
|
|
186
|
+
|
|
187
|
+
}else{
|
|
188
|
+
// doc does not exists, generate a new one
|
|
189
|
+
let new_val= {value:new_data.value}
|
|
190
|
+
|
|
191
|
+
if (new_data.on_update_array){
|
|
192
|
+
// this indicates the provided value is initial value inside the array
|
|
193
|
+
new_val.value = [new_data.value]
|
|
194
|
+
new_val.on_update_array = new_data.on_update_array
|
|
119
195
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
196
|
+
let new_doc = this._get_blank_doc("system_settings")
|
|
197
|
+
new_doc["data"] = {
|
|
198
|
+
"name": name,
|
|
199
|
+
...new_val
|
|
200
|
+
}
|
|
201
|
+
let d = await this.db_api.insert(new_doc)
|
|
202
|
+
return d
|
|
123
203
|
}
|
|
124
204
|
}
|
|
125
205
|
|
|
206
|
+
|
|
126
207
|
/**
|
|
127
208
|
* Adds indexes for all the schemas in the data base. This is important to make search faster. This must be done every time a new schema is introduced in the database
|
|
128
209
|
*/
|
|
129
210
|
async update_indexes() {
|
|
211
|
+
this._check_ready_to_use()
|
|
130
212
|
// @TODO check this. i don't the index created this way are actually useful in search.
|
|
131
213
|
let all_schemas_docs = await this.db_api.search({
|
|
132
214
|
selector: { schema: "schema" },
|
|
@@ -153,41 +235,74 @@ class BeanBagDB {
|
|
|
153
235
|
//const validate = ajv.compile(schema_obj);
|
|
154
236
|
//const valid = validate(data_obj);
|
|
155
237
|
if (!valid) {
|
|
156
|
-
|
|
157
|
-
throw new Error(validate.errors);
|
|
238
|
+
throw new ValidationError(validate.errors);
|
|
158
239
|
}
|
|
159
240
|
}
|
|
160
241
|
|
|
161
242
|
validate_schema_object(schema_doc){
|
|
162
|
-
let errors = []
|
|
243
|
+
let errors = [{"message":"Schema validation errors "}]
|
|
163
244
|
if(!schema_doc["schema"]["type"]){
|
|
164
|
-
errors.push("Schema must have the field schema.'type' which can only be 'object' ")
|
|
245
|
+
errors.push({message:"Schema must have the field schema.'type' which can only be 'object' "})
|
|
165
246
|
}else{
|
|
166
247
|
if(schema_doc["schema"]["type"]!="object"){
|
|
167
|
-
errors.push("The schema.'type' value is invalid.Only 'object' allowed")
|
|
248
|
+
errors.push({message:"The schema.'type' value is invalid.Only 'object' allowed"})
|
|
168
249
|
}
|
|
169
250
|
}
|
|
170
251
|
if(!schema_doc["schema"]["properties"]){
|
|
171
|
-
errors.push("The schema.'properties' object does not exists")
|
|
252
|
+
errors.push({message:"The schema.'properties' object does not exists"})
|
|
172
253
|
}else{
|
|
173
254
|
if(typeof(schema_doc["schema"]["properties"])!="object"){
|
|
174
|
-
errors.push("Invalid schema.properties. It must be an object and must have atleast one field inside.")
|
|
255
|
+
errors.push({message:"Invalid schema.properties. It must be an object and must have atleast one field inside."})
|
|
175
256
|
}
|
|
176
257
|
if(Object.keys(schema_doc["schema"]["properties"]).length==0){
|
|
177
|
-
errors.push("You must define at least one property")
|
|
258
|
+
errors.push({message:"You must define at least one property"})
|
|
178
259
|
}
|
|
179
260
|
}
|
|
180
261
|
|
|
181
262
|
if(!schema_doc["schema"]["additionalProperties"]){
|
|
182
|
-
errors.push("The schema.'additionalProperties' field is required")
|
|
263
|
+
errors.push({message:"The schema.'additionalProperties' field is required"})
|
|
183
264
|
}else{
|
|
184
265
|
if(typeof(schema_doc["schema"]["additionalProperties"])!="boolean"){
|
|
185
|
-
errors.push("Invalid schema.additionalProperties. It must be a boolean value")
|
|
266
|
+
errors.push({message:"Invalid schema.additionalProperties. It must be a boolean value"})
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const allKeys = Object.keys(schema_doc["schema"]["properties"])
|
|
271
|
+
if(schema_doc["settings"]["primary_keys"].length>0){
|
|
272
|
+
// check if all keys belong to the schema and are not of type object
|
|
273
|
+
let all_pk_exist = schema_doc["settings"]["primary_keys"].every(item=>allKeys.includes(item)&&schema_doc["schema"]["properties"][item]["type"]!="object"&&schema_doc["schema"]["properties"][item]["type"]!="array")
|
|
274
|
+
|
|
275
|
+
if(!all_pk_exist){
|
|
276
|
+
errors.push({message:"Primary keys invalid. All keys must be defined in the schema and must be non object"})
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
if(schema_doc["settings"]["non_editable_fields"].length>0){
|
|
282
|
+
// check if all keys belong to the schema
|
|
283
|
+
let all_ne_exist = schema_doc["settings"]["non_editable_fields"].every(item=>allKeys.includes(item))
|
|
284
|
+
if(!all_ne_exist){
|
|
285
|
+
errors.push({message:"Non editable fields invalid. All fields must be defined in the schema "})
|
|
186
286
|
}
|
|
187
287
|
}
|
|
188
288
|
|
|
189
|
-
if(
|
|
190
|
-
|
|
289
|
+
if(schema_doc["settings"]["encrypted_fields"].length>0){
|
|
290
|
+
// check if all keys belong to the schema and are only string
|
|
291
|
+
let all_enc_exist = schema_doc["settings"]["encrypted_fields"].every(item=>allKeys.includes(item)&&schema_doc["schema"]["properties"][item]["type"]=="string")
|
|
292
|
+
if(!all_enc_exist){
|
|
293
|
+
errors.push({message:"Invalid encrypted fields. All fields must be defined in the schema and must be string "})
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// check : primary keys cannot be encrypted
|
|
297
|
+
let all_enc_no_pk = schema_doc["settings"]["encrypted_fields"].every(item=>!schema_doc["settings"]["primary_keys"].includes(item))
|
|
298
|
+
if(!all_enc_no_pk){
|
|
299
|
+
errors.push({message:"Invalid encrypted fields.Primary key fields cannot be encrypted "})
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/// cannot encrypt primary field keys
|
|
304
|
+
if(errors.length>1){
|
|
305
|
+
throw new ValidationError(errors)
|
|
191
306
|
}
|
|
192
307
|
}
|
|
193
308
|
|
|
@@ -198,9 +313,10 @@ class BeanBagDB {
|
|
|
198
313
|
* @returns {Object} {doc} or {doc,schema}
|
|
199
314
|
*/
|
|
200
315
|
async get(doc_id,include_schema=false) {
|
|
316
|
+
this._check_ready_to_use()
|
|
201
317
|
let doc = await this.db_api.get(doc_id);
|
|
202
318
|
let schema = await this.get_schema_doc(doc.schema);
|
|
203
|
-
doc = this._decrypt_doc(schema, doc);
|
|
319
|
+
doc = this._decrypt_doc(schema["data"], doc);
|
|
204
320
|
if(include_schema){
|
|
205
321
|
return {doc,schema}
|
|
206
322
|
}
|
|
@@ -215,10 +331,11 @@ class BeanBagDB {
|
|
|
215
331
|
let schemaSearch = await this.db_api.search({
|
|
216
332
|
selector: { schema: "schema", "data.name": schema_name },
|
|
217
333
|
});
|
|
334
|
+
// console.log(schemaSearch)
|
|
218
335
|
if (schemaSearch.docs.length == 0) {
|
|
219
|
-
throw new Error(
|
|
336
|
+
throw new Error(this.error_codes.schema_not_found);
|
|
220
337
|
}
|
|
221
|
-
return schemaSearch.docs[0]
|
|
338
|
+
return schemaSearch.docs[0];
|
|
222
339
|
}
|
|
223
340
|
|
|
224
341
|
/**
|
|
@@ -230,7 +347,9 @@ class BeanBagDB {
|
|
|
230
347
|
* @returns object
|
|
231
348
|
*/
|
|
232
349
|
async get_doc(schema_name, primary_key = {}) {
|
|
233
|
-
|
|
350
|
+
this._check_ready_to_use()
|
|
351
|
+
let schema_doc = await this.get_schema_doc(schema_name);
|
|
352
|
+
let s_doc = schema_doc["data"];
|
|
234
353
|
let doc_obj;
|
|
235
354
|
if (
|
|
236
355
|
s_doc["settings"]["primary_keys"] &&
|
|
@@ -268,6 +387,7 @@ class BeanBagDB {
|
|
|
268
387
|
* @param {Object} criteria
|
|
269
388
|
*/
|
|
270
389
|
async search(criteria) {
|
|
390
|
+
this._check_ready_to_use()
|
|
271
391
|
if (!criteria["selector"]) {
|
|
272
392
|
throw new Error("Invalid search query.");
|
|
273
393
|
}
|
|
@@ -285,12 +405,14 @@ class BeanBagDB {
|
|
|
285
405
|
* @param {Object} settings (optional)
|
|
286
406
|
*/
|
|
287
407
|
async insert(schema, data, meta= {},settings = {}) {
|
|
408
|
+
//console.log("here in insert")
|
|
409
|
+
this._check_ready_to_use()
|
|
288
410
|
try {
|
|
289
411
|
let doc_obj = await this._insert_pre_checks(schema, data, settings);
|
|
290
412
|
let new_rec = await this.db_api.insert(doc_obj);
|
|
291
413
|
return { id: new_rec["id"] };
|
|
292
414
|
} catch (error) {
|
|
293
|
-
console.log(error);
|
|
415
|
+
// console.log(error);
|
|
294
416
|
throw error;
|
|
295
417
|
}
|
|
296
418
|
}
|
|
@@ -312,13 +434,14 @@ class BeanBagDB {
|
|
|
312
434
|
* @param {*} schema_name
|
|
313
435
|
* @param {doc_obj} updates {data:{},meta:{}}, need not be the full document, just the new values of all/some fields
|
|
314
436
|
* @param {Boolean} save_conflict = true -
|
|
315
|
-
*
|
|
437
|
+
*
|
|
316
438
|
*/
|
|
317
439
|
async update(doc_id, rev_id, updates, update_source="api",save_conflict = true) {
|
|
440
|
+
this._check_ready_to_use()
|
|
318
441
|
// making a big assumption here : primary key fields cannot be edited
|
|
319
442
|
// so updating the doc will not generate primary key conflicts
|
|
320
443
|
let req_data = await this.get(doc_id,true);
|
|
321
|
-
let schema = req_data.schema
|
|
444
|
+
let schema = req_data.schema
|
|
322
445
|
let full_doc = req_data.doc // await this.get(doc_id)["doc"];
|
|
323
446
|
|
|
324
447
|
// @TODO fix this : what to do if the rev id does not match
|
|
@@ -329,43 +452,37 @@ class BeanBagDB {
|
|
|
329
452
|
// }
|
|
330
453
|
// }
|
|
331
454
|
|
|
332
|
-
// blank check
|
|
333
|
-
|
|
334
455
|
// update new value depending on settings.editable_fields (if does not exists, all fields are editable)
|
|
335
|
-
let
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
}
|
|
456
|
+
let all_fields = Object.keys(schema.schema.properties)
|
|
457
|
+
let unedit_fields = schema.settings["non_editable_fields"]
|
|
458
|
+
let edit_fields = all_fields.filter(item=>!unedit_fields.includes(item))
|
|
339
459
|
|
|
340
460
|
// now generate the new doc with updates
|
|
341
461
|
let allowed_updates = this._filterObject(updates.data,edit_fields);
|
|
342
462
|
let updated_data = { ...full_doc.data, ...allowed_updates };
|
|
343
463
|
|
|
344
|
-
// validate data
|
|
345
464
|
this.validate_data(schema.schema, updated_data);
|
|
346
465
|
|
|
347
466
|
// primary key check if multiple records can be created
|
|
348
|
-
if(schema.settings["single_record"]==false){
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
if(search.docs.length
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
throw new Error("Update not allowed. Document with the same primary key already exists")
|
|
359
|
-
}
|
|
360
|
-
}else{
|
|
361
|
-
throw new Error("There is something wrong with the schema")
|
|
467
|
+
if(schema.settings["single_record"]==false && schema.settings["primary_keys"].length>0){
|
|
468
|
+
let pri_fields = schema.settings["primary_keys"]
|
|
469
|
+
let search_criteria = {schema:schema.name}
|
|
470
|
+
pri_fields.map(itm=>{search_criteria["data."+itm] = updated_data[itm]})
|
|
471
|
+
let search = await this.search({selection:search_criteria})
|
|
472
|
+
if(search.docs.length>0){
|
|
473
|
+
if(search.docs.length==1){
|
|
474
|
+
let thedoc = search.docs[0]
|
|
475
|
+
if(thedoc["_id"]!=doc_id){
|
|
476
|
+
throw new DocUpdateError([{message:"Update not allowed. Document with the same primary key already exists"}])
|
|
362
477
|
}
|
|
363
478
|
}
|
|
364
|
-
|
|
479
|
+
else{
|
|
480
|
+
throw new Error("There is something wrong with the schema primary keys")
|
|
481
|
+
}
|
|
482
|
+
}
|
|
365
483
|
}
|
|
366
484
|
|
|
367
485
|
// encrypt the data
|
|
368
|
-
|
|
369
486
|
full_doc["data"] = updated_data
|
|
370
487
|
full_doc = this._encrypt_doc(schema,full_doc);
|
|
371
488
|
|
|
@@ -384,11 +501,13 @@ class BeanBagDB {
|
|
|
384
501
|
}
|
|
385
502
|
|
|
386
503
|
async delete(doc_id) {
|
|
504
|
+
this._check_ready_to_use()
|
|
387
505
|
await this.db_api.delete(doc_id)
|
|
388
506
|
}
|
|
389
507
|
|
|
390
508
|
|
|
391
509
|
async load_plugin(plugin_name,plugin_module){
|
|
510
|
+
this._check_ready_to_use()
|
|
392
511
|
this.plugins[plugin_name] = {}
|
|
393
512
|
for (let func_name in plugin_module){
|
|
394
513
|
if(typeof plugin_module[func_name]=='function'){
|
|
@@ -401,24 +520,39 @@ class BeanBagDB {
|
|
|
401
520
|
}
|
|
402
521
|
}
|
|
403
522
|
|
|
404
|
-
////////
|
|
523
|
+
//////// Internal methods ////////
|
|
524
|
+
|
|
525
|
+
_get_current_version(){
|
|
526
|
+
// current version is the sum of versions of all system defined schemas
|
|
527
|
+
let sum = sys_sch.schema_schema.version
|
|
528
|
+
let keys = Object.keys(sys_sch.system_schemas).map(item=>{
|
|
529
|
+
sum = sum+ sys_sch.system_schemas[item].version
|
|
530
|
+
})
|
|
531
|
+
if(sum == NaN){
|
|
532
|
+
throw Error("Error in system schema version numbers")
|
|
533
|
+
}
|
|
534
|
+
return sum
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
_check_ready_to_use(){
|
|
538
|
+
if(!this.active){
|
|
539
|
+
throw new Error(this.error_codes.not_active)
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
405
543
|
|
|
406
544
|
_generate_random_link(){
|
|
407
545
|
const dictionary = ['rain', 'mars', 'banana', 'earth', 'kiwi', 'mercury', 'fuji', 'hurricane', 'matterhorn', 'snow', 'saturn', 'jupiter', 'peach', 'wind', 'pluto', 'apple', 'k2', 'storm', 'venus', 'denali', 'cloud', 'sunshine', 'mango', 'drizzle', 'pineapple', 'aconcagua', 'gasherbrum', 'apricot', 'neptune', 'fog', 'orange', 'blueberry', 'kilimanjaro', 'uranus', 'grape', 'storm', 'montblanc', 'lemon', 'chooyu', 'raspberry', 'cherry', 'thunder', 'vinson', 'breeze', 'elbrus', 'everest', 'parbat', 'makalu', 'nanga', 'kangchenjunga', 'lightning', 'cyclone', 'comet', 'asteroid', 'pomegranate', 'nectarine', 'clementine', 'strawberry', 'tornado', 'avalanche', 'andes', 'rockies', 'himalayas', 'pyrenees', 'carpathians', 'cascade', 'etna', 'vesuvius', 'volcano', 'tundra', 'whirlwind', 'iceberg', 'eclipse', 'zephyr', 'tropic', 'monsoon', 'aurora'];
|
|
408
546
|
return Array.from({ length: 4 }, () => dictionary[Math.floor(Math.random() * dictionary.length)]).join('-');
|
|
409
547
|
}
|
|
410
548
|
|
|
411
|
-
|
|
412
|
-
for (const field of requiredFields) {
|
|
413
|
-
if (!obj[field]) {throw new Error(`${field} is required`);}
|
|
414
|
-
}
|
|
415
|
-
}
|
|
549
|
+
|
|
416
550
|
|
|
417
551
|
/**
|
|
418
552
|
*
|
|
419
553
|
* @param {*} obj
|
|
420
554
|
* @param {*} fields
|
|
421
|
-
*
|
|
555
|
+
*
|
|
422
556
|
*/
|
|
423
557
|
_filterObject(obj, fields) {
|
|
424
558
|
return fields.reduce((filteredObj, field) => {
|
|
@@ -429,36 +563,7 @@ class BeanBagDB {
|
|
|
429
563
|
}, {});
|
|
430
564
|
}
|
|
431
565
|
|
|
432
|
-
|
|
433
|
-
* Checks if the selected database is initialized for working with BeanBagDB. Also throws a warning if package version does not match with database version.
|
|
434
|
-
* Every time a database is initialized, a setting document `beanbagdb_version` is added. If this does not exists, the database is not initialized. If it exists but does not match the current version, a warning is shown.
|
|
435
|
-
* @returns {object} {initialized:boolean,latest:boolean}
|
|
436
|
-
*/
|
|
437
|
-
async _check_ready_to_use() {
|
|
438
|
-
// @TODO check if ready to use in major API methods
|
|
439
|
-
let check = { initialized: false, latest: false };
|
|
440
|
-
// @TODO this is not really fool proof. check all the required docs, they have the system_generated flag
|
|
441
|
-
// what if some user mistakenly modifies or deletes some of the required docs ?
|
|
442
|
-
let version_search = await this.db_api.search({
|
|
443
|
-
selector: { schema: "system_settings", "data.name": "beanbagdb_version" },
|
|
444
|
-
});
|
|
445
|
-
if (version_search.docs.length > 0) {
|
|
446
|
-
let doc = version_search.docs[0];
|
|
447
|
-
check.initialized = true;
|
|
448
|
-
check.latest = doc["data"]["value"] == this._version;
|
|
449
|
-
}
|
|
450
|
-
if (check.initialized == false) {
|
|
451
|
-
console.warn(
|
|
452
|
-
"This database is not ready to be used. It is not initialized. Run `initialize_db()` first"
|
|
453
|
-
);
|
|
454
|
-
}
|
|
455
|
-
if ((check.latest == false) & (check.initialized == true)) {
|
|
456
|
-
console.warn(
|
|
457
|
-
"This database is not updated with the latest version. Run `initialize_db()` again to update to the latest version"
|
|
458
|
-
);
|
|
459
|
-
}
|
|
460
|
-
return check;
|
|
461
|
-
}
|
|
566
|
+
|
|
462
567
|
|
|
463
568
|
/**
|
|
464
569
|
* To update the system schema or reset to a stable version to ensure functioning of the BeanBagDB
|
|
@@ -488,7 +593,7 @@ class BeanBagDB {
|
|
|
488
593
|
let doc = {
|
|
489
594
|
data: {},
|
|
490
595
|
meta: {
|
|
491
|
-
|
|
596
|
+
created_on: this._get_now_unix_timestamp(),
|
|
492
597
|
tags: [],
|
|
493
598
|
app :{},
|
|
494
599
|
link : this._generate_random_link() // there is a link by default. overwrite this if user provided one but only before checking if it is unique
|
|
@@ -629,6 +734,54 @@ class BeanBagDB {
|
|
|
629
734
|
doc_obj["data"] = new_data;
|
|
630
735
|
return doc_obj;
|
|
631
736
|
}
|
|
737
|
+
|
|
738
|
+
////// Utility methods
|
|
739
|
+
util_check_required_fields(requiredFields,obj){
|
|
740
|
+
for (const field of requiredFields) {
|
|
741
|
+
if (!obj[field]) {throw new Error(`${field} is required`);}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
632
744
|
}
|
|
633
745
|
|
|
634
|
-
|
|
746
|
+
|
|
747
|
+
export class ValidationError extends Error {
|
|
748
|
+
constructor(errors = []) {
|
|
749
|
+
// Create a message based on the list of errors
|
|
750
|
+
//console.log(errors)
|
|
751
|
+
let error_messages = errors.map(item=>` ${(item.instancePath||" ").replace("/","")} ${item.message} `)
|
|
752
|
+
let message = `Validation failed with ${errors.length} error(s): ${error_messages.join(",")}`;
|
|
753
|
+
super(message);
|
|
754
|
+
this.name = 'ValidationError';
|
|
755
|
+
this.errors = errors; // Store the list of errors
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
export class DocUpdateError extends Error {
|
|
760
|
+
constructor(errors=[]){
|
|
761
|
+
let error_messages = errors.map(item=>`${item.message}`)
|
|
762
|
+
let message = `Error in document update. ${error_messages.join(",")}`
|
|
763
|
+
super(message)
|
|
764
|
+
this.name = "DocUpdateError";
|
|
765
|
+
this.errors = errors
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
export class DocInsertError extends Error {
|
|
770
|
+
constructor(errors=[]){
|
|
771
|
+
let error_messages = errors.map(item=>`${item.message}`)
|
|
772
|
+
let message = `Error in document insert. ${error_messages.join(",")}`
|
|
773
|
+
super(message)
|
|
774
|
+
this.name = "DocInsertError";
|
|
775
|
+
this.errors = errors
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
export class DocNotFoundError extends Error {
|
|
780
|
+
constructor(errors=[]){
|
|
781
|
+
let error_messages = errors.map(item=>`${item.message}`)
|
|
782
|
+
let message = `Error in fetching document. Criteria : ${error_messages.join(",")}`
|
|
783
|
+
super(message)
|
|
784
|
+
this.name = "DocNotFoundError";
|
|
785
|
+
this.errors = errors
|
|
786
|
+
}
|
|
787
|
+
}
|
package/src/system_schema.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
export const schema_schema = {
|
|
2
2
|
name: "schema",
|
|
3
|
-
description:"Meta-schema or schema for defining other schemas",
|
|
3
|
+
description:"Meta-schema or the schema for defining other schemas",
|
|
4
4
|
system_generated:true,
|
|
5
|
+
version:0.5,
|
|
5
6
|
schema: {
|
|
6
7
|
type: "object",
|
|
7
8
|
additionalProperties: false,
|
|
@@ -10,6 +11,12 @@ export const schema_schema = {
|
|
|
10
11
|
type:"boolean",
|
|
11
12
|
default:false
|
|
12
13
|
},
|
|
14
|
+
version: {
|
|
15
|
+
type: "number",
|
|
16
|
+
minimum: 0,
|
|
17
|
+
default: 1,
|
|
18
|
+
description:"This is an optional field.To be used primarily for system schemas"
|
|
19
|
+
},
|
|
13
20
|
name: {
|
|
14
21
|
type: "string",
|
|
15
22
|
minLength: 5,
|
|
@@ -41,14 +48,17 @@ export const schema_schema = {
|
|
|
41
48
|
type: "string",
|
|
42
49
|
},
|
|
43
50
|
maxItems: 10,
|
|
51
|
+
description:"Fields that makes each document unique in the schema.Leave it blank if you do not need it. You can still be able to distinguish documents using the link field and the document id."
|
|
44
52
|
},
|
|
45
|
-
|
|
53
|
+
non_editable_fields: {
|
|
46
54
|
type: "array",
|
|
47
55
|
default: [],
|
|
48
56
|
items: {
|
|
49
57
|
type: "string",
|
|
50
58
|
},
|
|
51
59
|
maxItems: 50,
|
|
60
|
+
minItems:0,
|
|
61
|
+
description:"The list of fields whose values are added when the document is created but cannot be edited later in future."
|
|
52
62
|
},
|
|
53
63
|
encrypted_fields: {
|
|
54
64
|
type: "array",
|
|
@@ -57,6 +67,7 @@ export const schema_schema = {
|
|
|
57
67
|
type: "string",
|
|
58
68
|
},
|
|
59
69
|
maxItems: 50,
|
|
70
|
+
description:"Once set, all the data in this field will be encrypted before storing it in the database. Encryption key must be provided during the time of BeanBagDB initialization and must be managed by the user as it is NOT stored in the database"
|
|
60
71
|
},
|
|
61
72
|
single_record: {
|
|
62
73
|
type: "boolean",
|
|
@@ -65,6 +76,7 @@ export const schema_schema = {
|
|
|
65
76
|
"If set, only a single records with this schema will be allowed to insert in the database",
|
|
66
77
|
},
|
|
67
78
|
},
|
|
79
|
+
required :["primary_keys","non_editable_fields","single_record","encrypted_fields"]
|
|
68
80
|
},
|
|
69
81
|
},
|
|
70
82
|
required: ["name","description","schema", "settings"],
|
|
@@ -76,29 +88,9 @@ export const schema_schema = {
|
|
|
76
88
|
};
|
|
77
89
|
|
|
78
90
|
export const system_schemas = {
|
|
79
|
-
logs: {
|
|
80
|
-
system_generated:true,
|
|
81
|
-
description:"Schema for the log doc. Single log doc for the whole DB to log stuff about the DB",
|
|
82
|
-
name: "system_logs",
|
|
83
|
-
schema: {
|
|
84
|
-
type: "object",
|
|
85
|
-
additionalProperties: true,
|
|
86
|
-
properties: {
|
|
87
|
-
logs: {
|
|
88
|
-
type: "array",
|
|
89
|
-
items: {
|
|
90
|
-
type: "object",
|
|
91
|
-
additionalProperties: true,
|
|
92
|
-
},
|
|
93
|
-
},
|
|
94
|
-
},
|
|
95
|
-
},
|
|
96
|
-
settings: {
|
|
97
|
-
single_record: true
|
|
98
|
-
},
|
|
99
|
-
},
|
|
100
91
|
keys: {
|
|
101
92
|
system_generated:true,
|
|
93
|
+
version:0.5,
|
|
102
94
|
description:"To store user defined key. this can include anything like API tokens etc. There is a special method to fetch this. The values are encrypted",
|
|
103
95
|
name: "system_keys",
|
|
104
96
|
schema: {
|
|
@@ -129,10 +121,13 @@ export const system_schemas = {
|
|
|
129
121
|
},
|
|
130
122
|
settings: {
|
|
131
123
|
primary_keys: ["name"],
|
|
132
|
-
encrypted_fields:["value"]
|
|
124
|
+
encrypted_fields:["value"],
|
|
125
|
+
non_editable_fields:[],
|
|
126
|
+
single_record: false
|
|
133
127
|
},
|
|
134
128
|
},
|
|
135
129
|
settings: {
|
|
130
|
+
version:0.5,
|
|
136
131
|
system_generated:true,
|
|
137
132
|
description:"The system relies on these settings for proper functioning or enabling optional features.",
|
|
138
133
|
name: "system_settings",
|
|
@@ -144,27 +139,25 @@ export const system_schemas = {
|
|
|
144
139
|
name: {
|
|
145
140
|
type: "string",
|
|
146
141
|
minLength: 5,
|
|
147
|
-
maxLength:
|
|
142
|
+
maxLength: 1000,
|
|
148
143
|
pattern: "^[a-zA-Z][a-zA-Z0-9_]*$",
|
|
149
144
|
},
|
|
150
145
|
value: {
|
|
151
|
-
type: "string",
|
|
152
|
-
minLength: 5,
|
|
153
|
-
maxLength: 5000,
|
|
154
|
-
pattern: "^[^\n\r]*$",
|
|
155
|
-
description:"Must be a single line string"
|
|
156
|
-
},
|
|
157
|
-
user_editable: {
|
|
158
|
-
type: "boolean",
|
|
159
|
-
default: true,
|
|
160
|
-
description:
|
|
161
|
-
"Whether this setting is editable by the user or only by the system",
|
|
146
|
+
type: ["string", "number", "boolean", "array"]
|
|
162
147
|
},
|
|
148
|
+
on_update_array:{
|
|
149
|
+
type:"string",
|
|
150
|
+
default:"replace",
|
|
151
|
+
description:"Special operation only for updating Arrays. Either replace it or append new elements to it. Cannot be edited",
|
|
152
|
+
enum:["replace","append"],
|
|
153
|
+
}
|
|
163
154
|
},
|
|
164
155
|
},
|
|
165
156
|
settings: {
|
|
166
157
|
primary_keys: ["name"],
|
|
167
|
-
|
|
158
|
+
non_editable_fields: ["name"],
|
|
159
|
+
encrypted_fields:[],
|
|
160
|
+
single_record:false
|
|
168
161
|
},
|
|
169
162
|
},
|
|
170
163
|
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import * as cdb from "./couchdb.js"
|
|
2
|
+
import "dotenv/config"
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
(async()=>{
|
|
9
|
+
|
|
10
|
+
let db = new cdb.BeanBagDB_CouchDB(process.env.cdburl, process.env.cdbname, process.env.secret)
|
|
11
|
+
|
|
12
|
+
//db.initialize_db()
|
|
13
|
+
await db.ready()
|
|
14
|
+
console.log(await db.metadata())
|
|
15
|
+
let test_schema = {
|
|
16
|
+
"name":"contact_list",
|
|
17
|
+
"description":"My contact book",
|
|
18
|
+
"schema":{},
|
|
19
|
+
"settings":{}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
//let sch = await db.insert("schema",test_schema,{"link":"test1"})
|
|
23
|
+
//console.log(sch)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
})();
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
package/test/couchdb.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// const crypto = require('crypto');
|
|
2
|
+
// const SDB = require("beanbagdb")
|
|
3
|
+
|
|
4
|
+
import Ajv from 'ajv';
|
|
5
|
+
import nano from "nano";
|
|
6
|
+
import BeanBagDB from '../src/index.js';
|
|
7
|
+
|
|
8
|
+
export class BeanBagDB_CouchDB extends BeanBagDB {
|
|
9
|
+
constructor(db_url,db_name,encryption_key){
|
|
10
|
+
//const cdb = import("nano")(db_url)
|
|
11
|
+
const cdb = nano(db_url)
|
|
12
|
+
const doc_obj = {
|
|
13
|
+
db_name:"couchdb",
|
|
14
|
+
name: db_name,
|
|
15
|
+
encryption_key: encryption_key,
|
|
16
|
+
api:{
|
|
17
|
+
insert: async (doc)=>{
|
|
18
|
+
const result = await cdb.insert(doc)
|
|
19
|
+
return result
|
|
20
|
+
},
|
|
21
|
+
// delete: ()=>{db1.destroy},
|
|
22
|
+
update: async (doc)=>{
|
|
23
|
+
const result = await cdb.insert(doc)
|
|
24
|
+
return result
|
|
25
|
+
},
|
|
26
|
+
search: async (query)=>{
|
|
27
|
+
const results = await cdb.find(query)
|
|
28
|
+
return results // of the form {docs:[],...}
|
|
29
|
+
},
|
|
30
|
+
get: async (id)=>{
|
|
31
|
+
const data = await cdb.get(id)
|
|
32
|
+
return data
|
|
33
|
+
},
|
|
34
|
+
createIndex: async (filter)=>{
|
|
35
|
+
const data = await cdb.createIndex(filter)
|
|
36
|
+
return data
|
|
37
|
+
},
|
|
38
|
+
delete: async (doc_id)=>{
|
|
39
|
+
const data = await cdb.get(id)
|
|
40
|
+
await cdb.destroy(data._id,data._rev)
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
utils:{
|
|
44
|
+
encrypt: (text,encryptionKey)=>{
|
|
45
|
+
const key = crypto.scryptSync(encryptionKey, 'salt', 32); // Derive a 256-bit key
|
|
46
|
+
const iv = crypto.randomBytes(16); // Initialization vector
|
|
47
|
+
const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
|
|
48
|
+
let encrypted = cipher.update(text, 'utf8', 'hex');
|
|
49
|
+
encrypted += cipher.final('hex');
|
|
50
|
+
return iv.toString('hex') + ':' + encrypted; // Prepend the IV for later use
|
|
51
|
+
},
|
|
52
|
+
decrypt : (encryptedText, encryptionKey)=>{
|
|
53
|
+
const key = crypto.scryptSync(encryptionKey, 'salt', 32); // Derive a 256-bit key
|
|
54
|
+
const [iv, encrypted] = encryptedText.split(':').map(part => Buffer.from(part, 'hex'));
|
|
55
|
+
const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
|
|
56
|
+
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
|
|
57
|
+
decrypted += decipher.final('utf8');
|
|
58
|
+
return decrypted;
|
|
59
|
+
},
|
|
60
|
+
ping : ()=>{
|
|
61
|
+
// @TODO ping the database to check connectivity when class is ready to use
|
|
62
|
+
},
|
|
63
|
+
validate_schema: (schema_obj, data_obj)=>{
|
|
64
|
+
const ajv = new Ajv({code: {esm: true}}) // options can be passed, e.g. {allErrors: true}
|
|
65
|
+
const validate = ajv.compile(schema_obj);
|
|
66
|
+
const valid = validate(data_obj);
|
|
67
|
+
return {valid,validate}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
super(doc_obj)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
|
package/test/init.test.js
CHANGED
package/test/operations.test.js
CHANGED
|
@@ -2,9 +2,7 @@
|
|
|
2
2
|
// to test initialization of the BeanBagDB class
|
|
3
3
|
import { get_pdb_doc } from './pouchdb.js';
|
|
4
4
|
import { throws, strictEqual,rejects } from "assert";
|
|
5
|
-
import BeanBagDB from '../src/index.js';
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
import {BeanBagDB,ValidationError} from '../src/index.js';
|
|
8
6
|
|
|
9
7
|
let database // this is the global db object
|
|
10
8
|
|
|
@@ -20,60 +18,64 @@ describe("Successful database class init (required for further testing) ", async
|
|
|
20
18
|
});
|
|
21
19
|
});
|
|
22
20
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
describe("Testing creation of schema docs", async () => {
|
|
21
|
+
describe("Schema doc insertion gives errors when", async () => {
|
|
27
22
|
let schema_docs_invalid = [
|
|
28
|
-
|
|
23
|
+
[ "name missing",
|
|
24
|
+
{
|
|
29
25
|
name: "",
|
|
30
26
|
description: "",
|
|
31
27
|
schema: {},
|
|
32
28
|
settings: {},
|
|
33
|
-
},
|
|
34
|
-
|
|
29
|
+
}],
|
|
30
|
+
["name is too short ",
|
|
31
|
+
{
|
|
35
32
|
name: "nos",
|
|
36
33
|
description: "",
|
|
37
34
|
schema: {},
|
|
38
35
|
settings: {},
|
|
39
|
-
},
|
|
40
|
-
|
|
36
|
+
}],
|
|
37
|
+
["schema is blank",
|
|
38
|
+
{
|
|
41
39
|
name: "contact",
|
|
42
40
|
description: "",
|
|
43
41
|
schema: {},
|
|
44
42
|
settings: {},
|
|
45
|
-
},
|
|
46
|
-
|
|
43
|
+
}],
|
|
44
|
+
["schema object missing",
|
|
45
|
+
{
|
|
47
46
|
name: "contact",
|
|
48
47
|
description: "This can be left blank",
|
|
49
|
-
schema: {},
|
|
50
48
|
settings: {},
|
|
51
|
-
},
|
|
52
|
-
|
|
49
|
+
}],
|
|
50
|
+
[
|
|
51
|
+
"no schema.type",{
|
|
53
52
|
name: "contact",
|
|
54
53
|
description: "This can be left blank",
|
|
55
54
|
schema: {
|
|
56
55
|
"abc":"something"
|
|
57
56
|
},
|
|
58
57
|
settings: {},
|
|
59
|
-
},
|
|
60
|
-
|
|
58
|
+
}],
|
|
59
|
+
["schema.type is invalid",
|
|
60
|
+
{
|
|
61
61
|
name: "contact",
|
|
62
62
|
description: "This can be left blank",
|
|
63
63
|
schema: {
|
|
64
64
|
"type":"something",
|
|
65
65
|
},
|
|
66
66
|
settings: {},
|
|
67
|
-
},
|
|
68
|
-
|
|
67
|
+
}],
|
|
68
|
+
["schema.properties is missing",
|
|
69
|
+
{
|
|
69
70
|
name: "contact",
|
|
70
71
|
description: "This can be left blank",
|
|
71
72
|
schema: {
|
|
72
73
|
"type":"object",
|
|
73
74
|
},
|
|
74
75
|
settings: {},
|
|
75
|
-
},
|
|
76
|
-
|
|
76
|
+
}],
|
|
77
|
+
["schema.properties is invalid",
|
|
78
|
+
{
|
|
77
79
|
name: "contact",
|
|
78
80
|
description: "This can be left blank",
|
|
79
81
|
schema: {
|
|
@@ -81,8 +83,9 @@ describe("Testing creation of schema docs", async () => {
|
|
|
81
83
|
"properties":"something"
|
|
82
84
|
},
|
|
83
85
|
settings: {},
|
|
84
|
-
},
|
|
85
|
-
|
|
86
|
+
}],
|
|
87
|
+
["schema.properties are missing/blank object",
|
|
88
|
+
{
|
|
86
89
|
name: "contact",
|
|
87
90
|
description: "This can be left blank",
|
|
88
91
|
schema: {
|
|
@@ -90,8 +93,9 @@ describe("Testing creation of schema docs", async () => {
|
|
|
90
93
|
"properties":{}
|
|
91
94
|
},
|
|
92
95
|
settings: {},
|
|
93
|
-
},
|
|
94
|
-
|
|
96
|
+
}],
|
|
97
|
+
["schema.additionalProperties is missing",
|
|
98
|
+
{
|
|
95
99
|
name: "contact",
|
|
96
100
|
description: "This can be left blank",
|
|
97
101
|
schema: {
|
|
@@ -99,8 +103,9 @@ describe("Testing creation of schema docs", async () => {
|
|
|
99
103
|
"properties":{"name":{"type":"string"}}
|
|
100
104
|
},
|
|
101
105
|
settings: {},
|
|
102
|
-
},
|
|
103
|
-
|
|
106
|
+
}],
|
|
107
|
+
["schema.additionalProperties is invalid",
|
|
108
|
+
{
|
|
104
109
|
name: "contact",
|
|
105
110
|
description: "This can be left blank",
|
|
106
111
|
schema: {
|
|
@@ -109,8 +114,30 @@ describe("Testing creation of schema docs", async () => {
|
|
|
109
114
|
"additionalProperties":"no"
|
|
110
115
|
},
|
|
111
116
|
settings: {},
|
|
112
|
-
},
|
|
113
|
-
|
|
117
|
+
}],
|
|
118
|
+
[
|
|
119
|
+
"setting is missing",{
|
|
120
|
+
name: "contact",
|
|
121
|
+
description: "This can be left blank",
|
|
122
|
+
schema: {
|
|
123
|
+
"type":"object",
|
|
124
|
+
"properties":{"name":{"type":"string"}},
|
|
125
|
+
"additionalProperties":true
|
|
126
|
+
}
|
|
127
|
+
}],
|
|
128
|
+
[
|
|
129
|
+
"setting is invalid",{
|
|
130
|
+
name: "contact",
|
|
131
|
+
description: "This can be left blank",
|
|
132
|
+
schema: {
|
|
133
|
+
"type":"object",
|
|
134
|
+
"properties":{"name":{"type":"string"}},
|
|
135
|
+
"additionalProperties":true
|
|
136
|
+
},
|
|
137
|
+
settings:"none"
|
|
138
|
+
}],
|
|
139
|
+
// ["settings.primary_keys is missing",
|
|
140
|
+
// {
|
|
114
141
|
// name: "contact",
|
|
115
142
|
// description: "This can be left blank",
|
|
116
143
|
// schema: {
|
|
@@ -118,28 +145,183 @@ describe("Testing creation of schema docs", async () => {
|
|
|
118
145
|
// "properties":{"name":{"type":"string"}},
|
|
119
146
|
// "additionalProperties":true
|
|
120
147
|
// },
|
|
121
|
-
// settings: {
|
|
122
|
-
//
|
|
148
|
+
// settings: {
|
|
149
|
+
// },
|
|
150
|
+
// }],
|
|
151
|
+
["settings.primary_keys is invalid",
|
|
152
|
+
{
|
|
153
|
+
name: "contact",
|
|
154
|
+
description: "This can be left blank",
|
|
155
|
+
schema: {
|
|
156
|
+
"type":"object",
|
|
157
|
+
"properties":{"name":{"type":"string"}},
|
|
158
|
+
"additionalProperties":true
|
|
159
|
+
},
|
|
160
|
+
settings: {
|
|
161
|
+
primary_keys:"name"
|
|
162
|
+
},
|
|
163
|
+
}],
|
|
164
|
+
["settings.non_editable_fields is invalid",
|
|
165
|
+
{
|
|
166
|
+
name: "contact",
|
|
167
|
+
description: "This can be left blank",
|
|
168
|
+
schema: {
|
|
169
|
+
"type":"object",
|
|
170
|
+
"properties":{"name":{"type":"string"}},
|
|
171
|
+
"additionalProperties":true
|
|
172
|
+
},
|
|
173
|
+
settings: {
|
|
174
|
+
primary_keys:["name"],
|
|
175
|
+
non_editable_fields:"all"
|
|
176
|
+
},
|
|
177
|
+
}],
|
|
178
|
+
["settings.single_record is invalid",
|
|
179
|
+
{
|
|
180
|
+
name: "contact",
|
|
181
|
+
description: "This can be left blank",
|
|
182
|
+
schema: {
|
|
183
|
+
"type":"object",
|
|
184
|
+
"properties":{"name":{"type":"string"}},
|
|
185
|
+
"additionalProperties":true
|
|
186
|
+
},
|
|
187
|
+
settings: {
|
|
188
|
+
primary_keys:["name"],
|
|
189
|
+
non_editable_fields:[],
|
|
190
|
+
single_record:"no"
|
|
191
|
+
},
|
|
192
|
+
}],
|
|
193
|
+
["settings.encrypted_fields is invalid",
|
|
194
|
+
{
|
|
195
|
+
name: "contact",
|
|
196
|
+
description: "This can be left blank",
|
|
197
|
+
schema: {
|
|
198
|
+
"type":"object",
|
|
199
|
+
"properties":{"name":{"type":"string"}},
|
|
200
|
+
"additionalProperties":true
|
|
201
|
+
},
|
|
202
|
+
settings: {
|
|
203
|
+
primary_keys:["name"],
|
|
204
|
+
non_editable_fields:[],
|
|
205
|
+
single_record:false,
|
|
206
|
+
encrypted_fields:"none"
|
|
207
|
+
},
|
|
208
|
+
}],
|
|
209
|
+
["settings.primary_keys fields are not defined in schema",
|
|
210
|
+
{
|
|
211
|
+
name: "contact",
|
|
212
|
+
description: "This can be left blank",
|
|
213
|
+
schema: {
|
|
214
|
+
"type":"object",
|
|
215
|
+
"properties":{"name":{"type":"string"}},
|
|
216
|
+
"additionalProperties":true
|
|
217
|
+
},
|
|
218
|
+
settings: {
|
|
219
|
+
primary_keys:["name1"],
|
|
220
|
+
non_editable_fields:[],
|
|
221
|
+
single_record:false,
|
|
222
|
+
encrypted_fields:"none"
|
|
223
|
+
},
|
|
224
|
+
}],
|
|
225
|
+
["settings.primary_keys field is an object",
|
|
226
|
+
{
|
|
227
|
+
name: "contact",
|
|
228
|
+
description: "This can be left blank",
|
|
229
|
+
schema: {
|
|
230
|
+
"type":"object",
|
|
231
|
+
"properties":{"name":{"type":"object"},"address":{type:"string"}},
|
|
232
|
+
"additionalProperties":true
|
|
233
|
+
},
|
|
234
|
+
settings: {
|
|
235
|
+
primary_keys:["name"],
|
|
236
|
+
non_editable_fields:[],
|
|
237
|
+
single_record:false,
|
|
238
|
+
encrypted_fields:[]
|
|
239
|
+
},
|
|
240
|
+
}],
|
|
241
|
+
["settings.non_editable_fields not defined in the schema",
|
|
242
|
+
{
|
|
243
|
+
name: "contact",
|
|
244
|
+
description: "This can be left blank",
|
|
245
|
+
schema: {
|
|
246
|
+
"type":"object",
|
|
247
|
+
"properties":{"name":{"type":"string"},"address":{type:"string"}},
|
|
248
|
+
"additionalProperties":true
|
|
249
|
+
},
|
|
250
|
+
settings: {
|
|
251
|
+
primary_keys:["name"],
|
|
252
|
+
non_editable_fields:["mobile"],
|
|
253
|
+
single_record:false,
|
|
254
|
+
encrypted_fields:[]
|
|
255
|
+
},
|
|
256
|
+
}],
|
|
257
|
+
["settings.encrypted_fields not defined in the schema",
|
|
258
|
+
{
|
|
259
|
+
name: "contact",
|
|
260
|
+
description: "This can be left blank",
|
|
261
|
+
schema: {
|
|
262
|
+
"type":"object",
|
|
263
|
+
"properties":{"name":{"type":"string"},"address":{type:"object"},"secret":{"type":"string"}},
|
|
264
|
+
"additionalProperties":true
|
|
265
|
+
},
|
|
266
|
+
settings: {
|
|
267
|
+
primary_keys:["name"],
|
|
268
|
+
non_editable_fields:["mobile"],
|
|
269
|
+
single_record:false,
|
|
270
|
+
encrypted_fields:["password"]
|
|
271
|
+
},
|
|
272
|
+
}],
|
|
273
|
+
["settings.encrypted_fields is not a string",
|
|
274
|
+
{
|
|
275
|
+
name: "contact",
|
|
276
|
+
description: "This can be left blank",
|
|
277
|
+
schema: {
|
|
278
|
+
"type":"object",
|
|
279
|
+
"properties":{"name":{"type":"string"},"address":{type:"object"},"secret":{"type":"string"}},
|
|
280
|
+
"additionalProperties":true
|
|
281
|
+
},
|
|
282
|
+
settings: {
|
|
283
|
+
primary_keys:["name"],
|
|
284
|
+
non_editable_fields:["mobile"],
|
|
285
|
+
single_record:false,
|
|
286
|
+
encrypted_fields:["address"]
|
|
287
|
+
},
|
|
288
|
+
}],
|
|
289
|
+
["settings.encrypted_fields is a primary key",
|
|
290
|
+
{
|
|
291
|
+
name: "contact",
|
|
292
|
+
description: "This can be left blank",
|
|
293
|
+
schema: {
|
|
294
|
+
"type":"object",
|
|
295
|
+
"properties":{"name":{"type":"string"},"address":{type:"object"},"secret":{"type":"string"}},
|
|
296
|
+
"additionalProperties":true
|
|
297
|
+
},
|
|
298
|
+
settings: {
|
|
299
|
+
primary_keys:["name"],
|
|
300
|
+
non_editable_fields:["mobile"],
|
|
301
|
+
single_record:false,
|
|
302
|
+
encrypted_fields:["name"]
|
|
303
|
+
},
|
|
304
|
+
}]
|
|
123
305
|
];
|
|
124
306
|
|
|
125
|
-
let doc_obj = get_pdb_doc("test_database_25", "qwertyuiopaqwsde1254");
|
|
126
|
-
let database = new BeanBagDB(doc_obj);
|
|
127
|
-
database.initialize_db();
|
|
128
|
-
database.ready();
|
|
129
|
-
// schema_docs_invalid.forEach(doc=>{})
|
|
130
|
-
for (let index = 0; index < schema_docs_invalid.length; index++) {
|
|
131
|
-
const element = schema_docs_invalid[index];
|
|
132
307
|
|
|
133
|
-
|
|
308
|
+
before(async () => {
|
|
309
|
+
let doc_obj = get_pdb_doc("test_database_25", "qwertyuiopaqwsde1254");
|
|
310
|
+
database = new BeanBagDB(doc_obj);
|
|
311
|
+
await database.ready(); // Ensure the database is ready before running tests
|
|
312
|
+
console.log("Ready for more tests...");
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
schema_docs_invalid.forEach((element, index) => {
|
|
316
|
+
it(`${element[0]}`, async () => {
|
|
134
317
|
await rejects(
|
|
135
318
|
async () => {
|
|
136
|
-
await database.insert("schema", element);
|
|
319
|
+
await database.insert("schema", element[1]);
|
|
137
320
|
},
|
|
138
|
-
|
|
321
|
+
ValidationError
|
|
139
322
|
);
|
|
140
323
|
});
|
|
141
|
-
|
|
142
|
-
}
|
|
324
|
+
});
|
|
143
325
|
|
|
144
326
|
});
|
|
145
327
|
|
package/test/pouchdb.js
CHANGED
package/test/test1.js
CHANGED
|
@@ -158,21 +158,29 @@
|
|
|
158
158
|
|
|
159
159
|
import { get_pdb_doc } from './pouchdb.js';
|
|
160
160
|
import { throws, strictEqual } from "assert";
|
|
161
|
-
import BeanBagDB from '../src/index.js';
|
|
161
|
+
import {BeanBagDB} from '../src/index.js';
|
|
162
162
|
|
|
163
163
|
(async()=>{
|
|
164
|
+
|
|
164
165
|
let schema_docs_invalid = [
|
|
165
166
|
{
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
167
|
+
name: "contact",
|
|
168
|
+
description: "This can be left blank",
|
|
169
|
+
schema: {
|
|
170
|
+
"type":"object",
|
|
171
|
+
"properties":{"name":{"type":"string"},"address":{type:"object"},"secret":{"type":"string"}},
|
|
172
|
+
"additionalProperties":true
|
|
173
|
+
},
|
|
174
|
+
settings: {
|
|
175
|
+
primary_keys:["name"],
|
|
176
|
+
non_editable_fields:["address"],
|
|
177
|
+
single_record:false,
|
|
178
|
+
encrypted_fields:["name"]
|
|
179
|
+
},
|
|
170
180
|
}
|
|
171
|
-
|
|
172
181
|
]
|
|
173
182
|
let doc_obj = get_pdb_doc("test_database_26","qwertyuiopaqwsde1254")
|
|
174
183
|
let database = new BeanBagDB(doc_obj);
|
|
175
|
-
database.
|
|
176
|
-
database.ready()
|
|
184
|
+
await database.ready()
|
|
177
185
|
let a = await database.insert("schema",schema_docs_invalid[0])
|
|
178
186
|
})()
|