beanbagdb 0.5.51 → 0.5.53

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/src/index.js CHANGED
@@ -1,205 +1,305 @@
1
1
  import * as sys_sch from "./system_schema.js";
2
2
  /**
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.
4
- * It takes a db_instance argument, which , this class relies on perform CRUD operations on the data.
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_
3
+ * The core BeanBagDB class abstracts the database logic making it adaptable to both frontend and backend application. It is designed to be independent of any specific database allowing for integration with many databases.
4
+ *
5
+ * **Initialization** : Upon initializing the `BeanBagDB` object, the user must pass a JSON object with essential parameters such as the encryption key. Access to the database is provided through the "api" object which should include asynchronous methods that handle basic CRUD operations and other utility functions.
6
+ *
7
+ * This class can serve a foundation for building database specific `BeanBagDb` classes.
8
+ *
9
+ * **Method types**
10
+
11
+ * The main methods allow users to interact with the database through the BeanBagDB class. All methods follow the snake_case naming convention (lowercase letters separated by underscores).
12
+ *
13
+ * The types of methods include:
14
+ * - Setup methods: These methods are used for setting up the system, such as '{@link ready}' and '{@link initialize}'.
15
+ * - CRUD methods: These methods handle basic database operations like '{@link create}', '{@link read}', '{@link update}', and '{@link delete}'.
16
+ * - Search methods: Like '{@link search}' to find multiple documents based on criteria. (Note: read fetches a single document, while search fetches multiple documents matching a criteria).
17
+ * - Plugin methods: These methods manage loading and using plugins (see {@tutorial plugins}).
18
+ * - Utility methods: These methods don't directly interact with the database but serve specific purposes, such as returning the current UTC timestamp. These methods are prefixed with util_.
19
+ *
20
+ * For more details, see the {@tutorial getting-started} tutorial.
21
+ * The class also includes internal methods intended for use within the class. These methods are not intended for external access and are prefixed with an underscore (they are not visible in the documentation, but you can check the source code).
10
22
  */
11
23
  export class BeanBagDB {
12
24
  /**
13
- * @param {object} db_instance - Database object
14
- * db_instance object contains 3 main keys :
15
- * - `name` : the name of the local database
16
- * - `encryption_key`: this is required for encrypting documents
17
- * - `api` : this is an object that must contain database specific functions. This includes : `insert(doc)`: takes a doc and runs the db insertion function, `update(updated_doc)` : gets the updated document and updates it in the DB, `search(query)`: takes a query to fetch data from the DB (assuming array of JSON is returned ), `get(id)`: takes a document id and returns its content, `createIndex(filter)`: to create an index in the database based on a filter
18
- * - `utils` : this includes `encrypt`, `decrypt`
25
+ * Initializes the BeanBagDB instance.
26
+ *
27
+ * @param {object} db_instance - Database configuration object.
28
+ * @param {string} db_instance.name - The name of the local database.
29
+ * @param {string} db_instance.encryption_key - A key for encrypting documents (minimum 20 characters).
30
+ * @param {object} db_instance.api - The API object containing database-specific CRUD operations.
31
+ * @param {function} db_instance.api.insert - Inserts a document into the database.
32
+ * @param {function} db_instance.api.update - Updates an existing document in the database.
33
+ * @param {function} db_instance.api.delete - Deletes a document from the database.
34
+ * @param {function} db_instance.api.search - Searches for documents based on a query (returns an array of JSON).
35
+ * @param {function} db_instance.api.get - Retrieves a document by its ID.
36
+ * @param {function} db_instance.api.createIndex - Creates an index in the database based on a filter.
37
+ * @param {object} db_instance.utils - Utility functions for encryption and other operations.
38
+ * @param {function} db_instance.utils.encrypt - Encrypts a document.
39
+ * @param {function} db_instance.utils.decrypt - Decrypts a document.
40
+ * @param {function} db_instance.utils.ping - Checks the database connection.
41
+ * @param {function} db_instance.utils.validate_schema - Validates the database schema.
19
42
  */
20
43
  constructor(db_instance) {
21
- // data validation checks
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)
44
+ this.util_check_required_fields(["name", "encryption_key", "api", "utils", "db_name"],db_instance)
45
+ this.util_check_required_fields(["insert", "update", "delete", "search", "get", "createIndex"],db_instance.api)
46
+ this.util_check_required_fields(["encrypt", "decrypt", "ping", "validate_schema"],db_instance.utils)
25
47
 
26
- if(db_instance.encryption_key.length<20){throw new Error("encryption_key must have at least 20 letters")}
27
- // db name should not be blank,
48
+ if (db_instance.encryption_key.length < 20) {
49
+ throw new ValidationError([{ message: BeanBagDB.error_codes.key_short }]);
50
+ }
28
51
 
29
52
  this.encryption_key = db_instance.encryption_key;
30
- this.db_name = db_instance.db_name // couchdb,pouchdb etc...
53
+ this.db_name = db_instance.db_name; // couchdb,pouchdb etc...
31
54
  this.db_api = db_instance.api;
32
55
  this.utils = db_instance.utils;
33
-
34
56
  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
43
-
57
+ database_name: db_instance.name,
58
+ backend_database: this.db_name,
59
+ beanbagdb_version_db: null,
60
+ };
61
+ this._version = this._get_current_version();
62
+ this.active = false;
63
+ this.plugins = {};
44
64
  console.log("Run ready() now");
45
-
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
65
  }
53
66
 
54
- async metadata(){
55
- // returns system data
67
+ /**
68
+ * Static property containing predefined error codes for common errors.
69
+ * These error codes can be used throughout the class to handle specific exceptions.
70
+ *
71
+ * @property {Object} error_codes - An object with key-value pairs representing error codes and their messages.
72
+ * @property {string} error_codes.key_short - The encryption key must contain at least 20 characters.
73
+ * @property {string} error_codes.not_active - The database is not ready. Run the `ready()` method first.
74
+ * @property {string} error_codes.schema_not_found - No schema found for the specified name.
75
+ * @property {string} error_codes.doc_not_found - No document found for the provided search criteria.
76
+ */
77
+ static error_codes = {
78
+ key_short: "The encryption key must at least contain 20 characters",
79
+ not_active: "Database is not ready. Run ready() first",
80
+ schema_not_found: "Schema not found",
81
+ doc_not_found: "Document with selected criteria does not exists",
82
+ };
83
+
84
+ ////////////////////////////////////////////////////////////////////////
85
+ //////////////////// Setup methods /////////////////////////////////////
86
+ ////////////////////////////////////////////////////////////////////////
87
+
88
+ /**
89
+ * Database object metadata
90
+ * @typedef {Object} DBMetaData
91
+ * @property {string} database_name - The name of the local database.
92
+ * @property {string} backend_database - The type of backend database (e.g., CouchDB, PouchDB).
93
+ * @property {number} beanbagdb_version_db - The version of the BeanBagDB in the database.
94
+ * @property {number} beanbagdb_version_code - The current version code of the BeanBagDB.
95
+ * @property {boolean} ready_to_use - Indicates whether the database is ready to be used (active state).
96
+ */
97
+
98
+ /**
99
+ * Retrieves metadata for the current database object.
100
+ * @return {DBMetaData} An object containing system metadata.
101
+ * @todo Include additional metadata: document count, schema count, records for each schema, size of the database.
102
+ */
103
+ metadata() {
104
+ // returns system data
56
105
  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,
106
+ ...this.meta,
107
+ beanbagdb_version_code: this._version,
108
+ ready_to_use: this.active,
109
+ };
62
110
  }
63
111
 
64
112
  /**
65
- * This is to check if the database is ready to be used. It it important to run this after the class is initialized.
113
+ * Checks if the database is ready to be used. It is important to run this method after the class is initialized.
114
+ *
115
+ * This method performs the following actions:
116
+ * - Pings the database.
117
+ * - Searches the database for the `system_settings.beanbagdb_version` document.
118
+ * - Sets the class state as active if the version matches the current BeanBagDB version.
119
+ * - If the version does not match, calls `initialize()` to set up the database to the latest version.
120
+ * @todo Code to ping the DB and throw Connection error if failed to connect
121
+ * @async
122
+ * @returns {Promise<void>} - Resolves when the database has been verified and initialized.
66
123
  */
67
124
  async ready() {
68
125
  // TODO Ping db
69
- let check = { initialized: false, latest: false ,db_version:null};
70
126
  let version_search = await this.db_api.search({
71
127
  selector: { schema: "system_settings", "data.name": "beanbagdb_version" },
72
128
  });
73
129
  if (version_search.docs.length > 0) {
74
130
  let doc = version_search.docs[0];
75
- this.active = doc["data"]["value"] == this._version;
76
- this.meta.beanbagdb_version_db = doc["data"]["value"]
131
+ this.active = doc["data"]["value"] == this._version;
132
+ this.meta.beanbagdb_version_db = doc["data"]["value"];
77
133
  }
78
- if(this.active){
79
- console.log("Ready")
80
- }else{
81
- await this.initialize_db()
134
+ if (this.active) {
135
+ console.log("Ready");
136
+ } else {
137
+ await this.initialize();
82
138
  }
83
139
  }
84
140
 
85
-
86
-
87
141
  /**
88
- * Initializes the database making it ready to be used. Typically, required to run after every time package is updated to a new version.
89
- * See the documentation on the architecture of the DB to understand what default schemas are required for a smooth functioning of the database
142
+ * Initializes the database with the required schemas.
143
+ *
144
+ * This method is responsible for:
145
+ * - Verifying the existence and latest version of the `schema_schema` document.
146
+ * - Upgrading or inserting a new system schema if the version is outdated or missing.
147
+ * - Logging initialization steps in the system logs.
148
+ * - Updating the database version if needed.
149
+ *
150
+ * This method is usually called automatically by '{@link ready}' if required but can be run manually if needed.
151
+ *
152
+ * @async
153
+ * @returns {Promise<void>} - Resolves when the initialization is complete.
154
+ * @throws {Error} - Throws an error if schema initialization fails.
90
155
  */
91
- async initialize_db() {
92
- // this works on its own but is usually called by ready automatically if required
156
+ async initialize() {
157
+ // this works on its own but is usually called by ready automatically if required
93
158
 
94
159
  // 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"]
160
+ let logs = ["init started"];
96
161
  try {
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)
162
+ let schema = await this.get("schema",{name:"schema"});
163
+ if (schema["data"]["version"] != sys_sch.schema_schema.version) {
164
+ logs.push("old schema_schema v " + schema["data"]["version"]);
165
+ let full_doc = await this.db_api.get(schema["_id"]);
166
+ full_doc["data"] = { ...sys_sch.schema_schema };
167
+ full_doc["meta"]["updated_on"] = this.util_get_now_unix_timestamp();
168
+ await this.db_api.update(full_doc);
169
+ logs.push("new schema_schema v " + sys_sch.schema_schema.version);
105
170
  }
106
-
107
171
  } catch (error) {
108
- console.log(error)
109
- if (error.message==this.error_codes.schema_not_found) {
110
- console.log("...adding new ")
172
+ // console.log(error);
173
+ if (error instanceof DocNotFoundError) {
111
174
  // inserting new schema_schema doc
112
175
  let schema_schema_doc = this._get_blank_doc("schema");
113
176
  schema_schema_doc.data = sys_sch.schema_schema;
114
177
  await this.db_api.insert(schema_schema_doc);
115
- logs.push("init schema_schema v "+sys_sch.schema_schema.version)
116
- }
178
+ logs.push("init schema_schema v " + sys_sch.schema_schema.version);
179
+ }
117
180
  }
118
181
 
119
182
  let keys = Object.keys(sys_sch.system_schemas);
120
183
  for (let index = 0; index < keys.length; index++) {
121
- const schema_name = sys_sch.system_schemas[keys[index]]["name"]
184
+ const schema_name = sys_sch.system_schemas[keys[index]]["name"];
122
185
  const schema_data = sys_sch.system_schemas[keys[index]];
123
186
  try {
124
187
  // 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)
188
+ let schema1 = await this.get("schema",{name:schema_name})
189
+ if (schema1["data"]["version"] != schema_data.version) {
190
+ logs.push("old " + schema_name + " v " + schema1["data"]["version"]);
191
+ let full_doc = await this.db_api.get(schema1["_id"]);
192
+ full_doc["data"] = { ...schema_data };
193
+ full_doc["meta"]["updated_on"] = this.util_get_now_unix_timestamp();
194
+ await this.db_api.update(full_doc);
195
+ logs.push("new " + schema_name + " v " + schema_data.version);
133
196
  }
134
197
  } catch (error) {
135
- console.log(error)
136
- if (error.message==this.error_codes.schema_not_found) {
198
+ // console.log(error);
199
+ if (error instanceof DocNotFoundError) {
137
200
  // inserting new schema doc
138
- let new_schema_doc = this._get_blank_schema_doc("schema",sys_sch.schema_schema["schema"],schema_data);
201
+ let new_schema_doc = this._get_blank_schema_doc(
202
+ "schema",
203
+ sys_sch.schema_schema["schema"],
204
+ schema_data
205
+ );
139
206
  await this.db_api.insert(new_schema_doc);
140
- logs.push("init "+schema_name+" v "+schema_data.version)
207
+ logs.push("init " + schema_name + " v " + schema_data.version);
141
208
  }
142
209
  }
143
210
  }
144
- // store the logs in the log_doc , generate it for the first time
211
+ // store the logs in the log_doc , generate it for the first time
145
212
  // console.log(logs)
146
- if(logs.length>1){
213
+ if (logs.length > 1) {
147
214
  // 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")
215
+ logs.push("Init done");
216
+
217
+ await this.save_setting_doc("system_logs", {
218
+ value: { text: logs.join(","), added: this.util_get_now_unix_timestamp() },
219
+ on_update_array: "append",
220
+ });
221
+ await this.save_setting_doc("beanbagdb_version", {
222
+ value: this._version,
223
+ });
224
+
225
+ this.meta.beanbagdb_version_db = this._version;
226
+ this.active = true;
227
+ console.log(logs.join(","))
228
+ } else {
229
+ // no new updates were done
230
+ console.log("Database already up to date");
159
231
  }
160
232
  }
161
- async insert_or_update_setting(name,new_data,schema={}){
233
+
234
+
235
+ /**
236
+ * Updates a setting document if it already exists in the database or creates a new document
237
+ * Inserts or updates a setting in the system settings schema.
238
+ *
239
+ * This method either:
240
+ * - Updates an existing document if the setting with the given `name` already exists in the database.
241
+ * - Inserts a new document if no matching setting is found.
242
+ *
243
+ * If the setting exists and the `value` is an array, the behavior depends on the `on_update_array` key:
244
+ * - `"append"`: Appends the new value to the existing array.
245
+ * - `"update"`: Replaces the current array with the new value.
246
+ *
247
+ * @async
248
+ * @param {string} name - The name of the setting to insert or update.
249
+ * @param {object} new_data - The new data to insert or update.
250
+ * @param {*} new_data.value - The value to insert or update.
251
+ * @param {string} [new_data.on_update_array] - Optional behavior for handling arrays, either "append" or "update".
252
+ * @param {object} [schema={}] - Optional schema to validate the data against (currently not implemented).
253
+ * @returns {Promise<object>} - The updated or newly inserted document.
254
+ * @throws {Error} - Throws an error if `new_data` or `new_data.value` is not provided, or if `on_update_array` is invalid.
255
+ */
256
+ async save_setting_doc(name, new_data, schema = {}) {
162
257
  // 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")
178
- }
179
- }else{
180
- doc["data"]["value"] = new_data.value
258
+ if (!new_data) {
259
+ throw new Error("No data provided");
260
+ }
261
+ if (!new_data.value) {
262
+ throw new Error("No value provided");
263
+ }
264
+
265
+ let doc_search = await this.db_api.search({
266
+ selector: { schema: "system_settings", "data.name": name },
267
+ });
268
+ if (doc_search.docs.length > 0) {
269
+ // doc already exists, check schema and update it : if it exists then it's value already exists and can be
270
+ let doc = { ...doc_search.docs[0] };
271
+ if (Array.isArray(doc.data.value)) {
272
+ let append_type = doc.data.on_update_array;
273
+ if (append_type == "append") {
274
+ doc["data"]["value"].push(new_data.value);
275
+ } else if (append_type == "update") {
276
+ doc["data"]["value"] = new_data.value;
277
+ } else {
278
+ throw new Error("Invalid on update array value");
181
279
  }
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}
280
+ } else {
281
+ doc["data"]["value"] = new_data.value;
282
+ }
283
+ // finally update it
284
+ doc["meta"]["updated_on"] = this.util_get_now_unix_timestamp();
285
+ await this.db_api.update(doc);
286
+ return doc;
287
+ } else {
288
+ // doc does not exists, generate a new one
289
+ let new_val = { value: new_data.value };
190
290
 
191
- if (new_data.on_update_array){
291
+ if (new_data.on_update_array) {
192
292
  // 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
293
+ new_val.value = [new_data.value];
294
+ new_val.on_update_array = new_data.on_update_array;
195
295
  }
196
- let new_doc = this._get_blank_doc("system_settings")
296
+ let new_doc = this._get_blank_doc("system_settings");
197
297
  new_doc["data"] = {
198
- "name": name,
199
- ...new_val
200
- }
201
- let d = await this.db_api.insert(new_doc)
202
- return d
298
+ name: name,
299
+ ...new_val,
300
+ };
301
+ let d = await this.db_api.insert(new_doc);
302
+ return d;
203
303
  }
204
304
  }
205
305
 
@@ -208,7 +308,7 @@ export class BeanBagDB {
208
308
  * 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
209
309
  */
210
310
  async update_indexes() {
211
- this._check_ready_to_use()
311
+ this._check_ready_to_use();
212
312
  // @TODO check this. i don't the index created this way are actually useful in search.
213
313
  let all_schemas_docs = await this.db_api.search({
214
314
  selector: { schema: "schema" },
@@ -222,163 +322,244 @@ export class BeanBagDB {
222
322
  await this.db_api.createIndex({ index: { fields: indexes } });
223
323
  }
224
324
 
325
+
326
+
327
+ ////////////////////////////////////////////////////////
328
+ /////////////// CRUD operations /////////////////////
329
+ ///////////////////////////////////////////////////////
330
+
225
331
  /**
226
- * Validates a data object against a provided JSON schema
227
- * It relies on the Ajv package to make the validation.
228
- * @param {Object} schema_obj - The JSON schema object to validate against
229
- * @param {Object} data_obj - The data object to validate
230
- * @throws {Error} If the data object does not conform to the schema
332
+ * Creates a document for the given schema into the database.
333
+ *
334
+ * This method validates the input data and schema before inserting a new document into the database.
335
+ *
336
+ * @async
337
+ * @param {string} schema - The schema name for the document, e.g., "contact".
338
+ * @param {object} data - The document data, e.g., { "name": "", "mobile": "", ... }.
339
+ * @param {object} [meta={}] - Optional metadata associated with the document.
340
+ * @param {object} [settings={}] - Optional settings that may affect document creation behavior.
341
+ * @returns {Promise<{id: string}>} - A promise that resolves with the newly inserted document's ID.
342
+ * @throws {Error} - Throws an error if insertion checks fail or if there is an issue with the database operation.
231
343
  */
232
- validate_data(schema_obj, data_obj) {
233
- const {valid,validate} = this.utils.validate_schema(schema_obj, data_obj)
234
- //const ajv = new Ajv({code: {esm: true}}) // options can be passed, e.g. {allErrors: true}
235
- //const validate = ajv.compile(schema_obj);
236
- //const valid = validate(data_obj);
237
- if (!valid) {
238
- throw new ValidationError(validate.errors);
344
+ async create(schema, data, meta = {}, settings = {}) {
345
+ this._check_ready_to_use();
346
+ if(!schema){throw new DocCreationError(`No schema provided`)}
347
+ if(Object.keys(data).length==0){throw new DocCreationError(`No data provided`)}
348
+ try {
349
+ let doc_obj = await this._insert_pre_checks(schema, data,meta, settings);
350
+ let new_rec = await this.db_api.insert(doc_obj);
351
+ return {_id:new_rec["id"],_rev : new_rec["rev"] ,...doc_obj};
352
+ } catch (error) {
353
+ throw error;
239
354
  }
240
355
  }
241
356
 
242
- validate_schema_object(schema_doc){
243
- let errors = [{"message":"Schema validation errors "}]
244
- if(!schema_doc["schema"]["type"]){
245
- errors.push({message:"Schema must have the field schema.'type' which can only be 'object' "})
246
- }else{
247
- if(schema_doc["schema"]["type"]!="object"){
248
- errors.push({message:"The schema.'type' value is invalid.Only 'object' allowed"})
249
- }
250
- }
251
- if(!schema_doc["schema"]["properties"]){
252
- errors.push({message:"The schema.'properties' object does not exists"})
253
- }else{
254
- if(typeof(schema_doc["schema"]["properties"])!="object"){
255
- errors.push({message:"Invalid schema.properties. It must be an object and must have atleast one field inside."})
256
- }
257
- if(Object.keys(schema_doc["schema"]["properties"]).length==0){
258
- errors.push({message:"You must define at least one property"})
259
- }
260
- }
357
+ /**
358
+ * Reads a document from the database based on the provided criteria.
359
+ *
360
+ * There are three valid ways to search for one document:
361
+ * 1. By `_id` (e.g., `{ "_id": "document_id" }`)
362
+ * 2. By `link` (e.g., `{ "link": "some_link" }`)
363
+ * 3. By schema's primary key (e.g., `{ "schema": "schema_name", "data": { "primary_key_1": "value", "primary_key_2": "value" }}`)
364
+ *
365
+ * If the document does not exist, an error will be thrown.
366
+ *
367
+ * @param {Object} criteria - The search criteria for the document.
368
+ * @param {string} [criteria._id] - The document ID for direct lookup.
369
+ * @param {string} [criteria.link] - A unique link identifier for the document.
370
+ * @param {string} [criteria.schema] - The schema name used when searching by primary keys.
371
+ * @param {Object} [criteria.data] - Data object containing the schema's primary keys for search.
372
+ *
373
+ * @param {boolean} [include_schema=false] - Whether to include the schema object in the returned result.
374
+ *
375
+ * @returns {Promise<Object>} - Returns an object with the document (`doc`) and optionally the schema (`schema`).
376
+ *
377
+ * @throws {DocNotFoundError} If no document is found for the given criteria.
378
+ * @throws {ValidationError} If invalid search criteria are provided.
379
+ */
380
+ async read(criteria, include_schema = false) {
381
+ // todo : decrypt doc
382
+ this._check_ready_to_use()
383
+ let obj = { doc: null }
384
+ let data_schema = null
385
+ if (criteria._id) {
386
+ try {
387
+ let doc = await this.db_api.get(criteria._id)
388
+ //console.log(doc)
389
+ obj.doc = doc;
390
+ } catch (error) { throw new DocNotFoundError(BeanBagDB.error_codes.doc_not_found)}
391
+ } else if (criteria.link) {
392
+ let linkSearch = await this.db_api.search({selector: { "meta.link": criteria.link }})
393
+ if (linkSearch.docs.length == 0) {throw new DocNotFoundError(BeanBagDB.error_codes.doc_not_found)}
394
+ obj.doc = linkSearch.docs[0];
395
+ } else if (criteria.hasOwnProperty("schema") & criteria.hasOwnProperty("data")) {
396
+ data_schema = await this.get("schema",{"name":criteria.schema})
397
+ let A = data_schema["data"]["settings"]["primary_keys"];
398
+ let search_criteria = { schema: criteria.schema };
399
+ A.forEach((itm) => {
400
+ if (!criteria["data"][itm]) {throw new ValidationError("Incomplete Primary key set. Required field(s) : " + A.join(","))}
401
+ search_criteria["data." + itm] = criteria["data"][itm];
402
+ });
261
403
 
262
- if(!schema_doc["schema"]["additionalProperties"]){
263
- errors.push({message:"The schema.'additionalProperties' field is required"})
264
- }else{
265
- if(typeof(schema_doc["schema"]["additionalProperties"])!="boolean"){
266
- errors.push({message:"Invalid schema.additionalProperties. It must be a boolean value"})
267
- }
268
- }
404
+ let pkSearch = await this.db_api.search({selector: search_criteria})
405
+ if (pkSearch.docs.length == 0) {throw new DocNotFoundError(BeanBagDB.error_codes.doc_not_found)}
406
+ obj.doc = pkSearch.docs[0]
269
407
 
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
- }
408
+ } else {
409
+ throw new ValidationError(`Invalid criteria to read a document. Valid ways : {"schema":"schema_name","data":{...primary key}} or {"_id":""} or {"link":""} `)
278
410
  }
411
+ if (!data_schema){
412
+ data_schema = await this.get("schema",{"name":obj.doc.schema})
413
+ }
414
+ if(include_schema) {obj.schema = data_schema["data"]}
415
+ // decrypt the document
416
+ obj.doc = await this._decrypt_doc(data_schema["data"], obj.doc)
417
+ return obj;
418
+ }
279
419
 
280
420
 
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 "})
421
+ /**
422
+ * Updates the data and metadata of a document.
423
+ *
424
+ * **Frequently Asked Questions**:
425
+ *
426
+ * - **Which data fields can be edited?**
427
+ * - All fields except for the ones listed in the schema's `settings.non_editable_fields` can be edited. If this setting is blank, all fields are editable by default.
428
+ *
429
+ * - **Are primary key fields editable?**
430
+ * - Yes, but a validation check ensures that primary key policies are not violated before the update is applied.
431
+ *
432
+ *
433
+ * @param {Object} doc_search_criteria - The criteria used to search for the document (e.g., {"_id": "document_id"}, {"link": "some_link"}, {"schema": "schema_name", "data": {primary_key_fields}}).
434
+ * @param {String} rev_id - The document's revision ID (`_rev`) used for version control and conflict detection.
435
+ * @param {Object} updates - The updated values for the document, structured as `{data: {}, meta: {}}`. Only the fields to be updated need to be provided.
436
+ * @param {String} [update_source="api"] - Identifies the source of the update (default: "api").
437
+ * @param {Boolean} [save_conflict=true] - If `true`, conflicting updates will be saved separately in case of revision mismatches.
438
+ *
439
+ * **Behavior**:
440
+ * - Retrieves the document based on the provided search criteria.
441
+ * - Checks the revision ID to detect potential conflicts. (To be implemented: behavior when the `rev_id` does not match).
442
+ * - Validates editable fields against `schema.settings.editable_fields` (or allows editing of all fields if not specified).
443
+ * - Performs primary key conflict checks if multiple records are allowed (`single_record == false`).
444
+ * - Encrypts fields if encryption is required by the schema settings.
445
+ * - Updates the `meta` fields (such as `updated_on` and `updated_by`) and saves the updated document to the database.
446
+ *
447
+ * **Returns**:
448
+ * @returns {Object} The result of the document update operation.
449
+ *
450
+ * **Errors**:
451
+ * - Throws an error if a document with the same primary keys already exists (and `single_record == false`).
452
+ * - Throws a `DocUpdateError` if a primary key conflict is detected during the update.
453
+ *
454
+ * @throws {DocUpdateError} - If a document with conflicting primary keys already exists.
455
+ * @throws {ValidationError} - If the provided data or metadata is invalid according to the schema.
456
+ */
457
+ async update(doc_search_criteria, updates, rev_id="", update_source = "api", save_conflict = true) {
458
+ this._check_ready_to_use();
459
+ // making a big assumption here : primary key fields cannot be edited
460
+ // so updating the doc will not generate primary key conflicts
461
+ let req_data = await this.read(doc_search_criteria, true);
462
+ let schema = req_data.schema;
463
+ let full_doc = req_data.doc;
464
+ // @TODO fix this : what to do if the rev id does not match
465
+ // if (full_doc["_rev"] != rev_id) {
466
+ // // throw error , save conflicting doc separately by default
467
+ // if (save_conflict) {
468
+ // // save conflicting doc todo
469
+ // }
470
+ // }
471
+
472
+ // update new value depending on settings.non_editable_fields (if does not exists, all fields are editable)
473
+ let all_fields = Object.keys(schema.schema.properties);
474
+ let unedit_fields = schema.settings["non_editable_fields"];
475
+ let edit_fields = all_fields.filter((item) => !unedit_fields.includes(item))
476
+ // now generate the new doc with updates
477
+ let something_to_update = false
478
+ let allowed_updates = this.util_filter_object(updates.data||{}, edit_fields);
479
+ if(Object.keys(allowed_updates).length>0){
480
+ // todo : what if additionalField are allowed ??
481
+ let updated_data = { ...full_doc.data, ...allowed_updates };
482
+
483
+ this.util_validate_data(schema.schema, updated_data);
484
+
485
+ // primary key check if multiple records can be created
486
+ if (schema.settings["primary_keys"].length > 0) {
487
+ let pri_fields = schema.settings["primary_keys"];
488
+ let search_criteria = { schema: schema.name };
489
+ pri_fields.map((itm) => {search_criteria["data." + itm] = updated_data[itm];});
490
+ let search = await this.search({ selector: search_criteria });
491
+ if (search.docs.length > 0) {
492
+ if (search.docs.length == 1) {
493
+ let thedoc = search.docs[0];
494
+ if (thedoc["_id"] != full_doc._id) {
495
+ throw new DocUpdateError("Update not allowed. Document with the same primary key already exists");
496
+ }
497
+ } else {
498
+ throw new Error("There is something wrong with the schema primary keys");
499
+ }
500
+ }
286
501
  }
502
+
503
+ full_doc["data"] = updated_data;
504
+ something_to_update = true
287
505
  }
288
-
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 "})
506
+ if (updates.meta) {
507
+ let m_sch = sys_sch.editable_metadata_schema;
508
+ let editable_fields = Object.keys(m_sch["properties"]);
509
+ let allowed_meta = this.util_filter_object(updates.meta, editable_fields);
510
+ this.util_validate_data(m_sch, allowed_meta);
511
+ // if update has a link ,then check if it already exists
512
+ if (allowed_meta.link){
513
+ let search = await this.search({ selector: {"meta.link":allowed_meta.link} })
514
+ if (search.docs.length > 0) {
515
+ if (search.docs.length == 1) {
516
+ let thedoc = search.docs[0];
517
+ if (thedoc["_id"] != full_doc._id) {
518
+ throw new DocUpdateError("Update not allowed. Document with the same link already exists");
519
+ }
520
+ } else {throw new Error("There is something wrong.")}
521
+ }
294
522
  }
295
523
 
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
- }
524
+ full_doc["meta"] = { ...full_doc["meta"], ...allowed_meta };
525
+ something_to_update = true
301
526
  }
302
-
303
- /// cannot encrypt primary field keys
304
- if(errors.length>1){
305
- throw new ValidationError(errors)
527
+ if(something_to_update){
528
+ // encrypt the data again since read returns decrypted data
529
+ full_doc = await this._encrypt_doc(schema, full_doc);
530
+ full_doc.meta["updated_on"] = this.util_get_now_unix_timestamp();
531
+ full_doc.meta["updated_by"] = update_source;
532
+ // console.log(full_doc)
533
+ let up = await this.db_api.update(full_doc);
534
+ return up;
535
+ }else{
536
+ throw new DocUpdateError("Nothing to update")
306
537
  }
307
538
  }
308
539
 
309
- /**
310
- * Returns a document with the provided ID
311
- * @param {String} doc_id - the doc Id (not the primary key)
312
- * @param {Boolean} include_schema - whether to include the schema doc as well
313
- * @returns {Object} {doc} or {doc,schema}
314
- */
315
- async get(doc_id,include_schema=false) {
316
- this._check_ready_to_use()
317
- let doc = await this.db_api.get(doc_id);
318
- let schema = await this.get_schema_doc(doc.schema);
319
- doc = this._decrypt_doc(schema["data"], doc);
320
- if(include_schema){
321
- return {doc,schema}
322
- }
323
- return {doc};
324
- }
325
540
 
326
- /**
327
- * Returns schema document for the given schema name s
328
- * @param {String} schema_name - Schema name
329
- */
330
- async get_schema_doc(schema_name) {
331
- let schemaSearch = await this.db_api.search({
332
- selector: { schema: "schema", "data.name": schema_name },
333
- });
334
- // console.log(schemaSearch)
335
- if (schemaSearch.docs.length == 0) {
336
- throw new Error(this.error_codes.schema_not_found);
541
+ /**
542
+ * Deletes a document from the database by its ID.
543
+ *
544
+ * @param {String} doc_id - The ID of the document to delete.
545
+ * @throws {DocNotFoundError} If the document with the specified ID does not exist.
546
+ * @throws {ValidationError} If the database is not ready to use.
547
+ */
548
+ async delete(criteria) {
549
+ this._check_ready_to_use();
550
+ let doc = await this.read(criteria)
551
+ const delete_blocked = ["schema","setting",""]
552
+ if (delete_blocked.includes(doc.schema)){
553
+ throw new Error(`Deletion of ${doc.schema} doc is not support yet.`)
337
554
  }
338
- return schemaSearch.docs[0];
555
+ await this.db_api.delete(doc.doc._id);
339
556
  }
340
557
 
341
- /**
342
- * Fetches a document based on a given schema and primary key.
343
- * In case schema has a single record, leave the primary_key blank `[]`
344
- * Can also be used to get special system docs such as settings
345
- * @param {String} schema_name
346
- * @param {Object} primary_key
347
- * @returns object
348
- */
349
- async get_doc(schema_name, primary_key = {}) {
350
- this._check_ready_to_use()
351
- let schema_doc = await this.get_schema_doc(schema_name);
352
- let s_doc = schema_doc["data"];
353
- let doc_obj;
354
- if (
355
- s_doc["settings"]["primary_keys"] &&
356
- s_doc["settings"]["primary_keys"].length > 0
357
- ) {
358
- let A = s_doc["settings"]["primary_keys"];
359
- let search_criteria = { schema: schema_name };
360
- A.forEach((itm) => {
361
- if (!primary_key[itm]) {
362
- throw new Error(
363
- "Incomplete Primary key set. Required field(s) : " + A.join(",")
364
- );
365
- }
366
- search_criteria["data." + itm] = primary_key[itm];
367
- });
368
- let s = await this.search({ selector: search_criteria });
369
- doc_obj = s.docs[0];
370
- } else {
371
- let s = await this.search({ selector: { schema: schema_name } });
372
- if (s.docs.length > 1) {
373
- throw new Error(
374
- "Invalid schema. At least one primary key must be defined or set the singleRecord option to true. "
375
- );
376
- }
377
- doc_obj = s.docs[0];
378
- }
379
- doc_obj = this._decrypt_doc(s_doc, doc_obj);
380
- return doc_obj;
381
- }
558
+
559
+ ////////////////////////////////////////////////////////
560
+ ////////////////// Search ////////////////////////////
561
+ ///////////////////////////////////////////////////////
562
+
382
563
 
383
564
  /**
384
565
  * Searches for documents in the database for the specified query. The query are Mango queries.
@@ -386,203 +567,112 @@ export class BeanBagDB {
386
567
  * E.g
387
568
  * @param {Object} criteria
388
569
  */
389
- async search(criteria) {
390
- this._check_ready_to_use()
570
+ async search(criteria={}) {
571
+ this._check_ready_to_use();
391
572
  if (!criteria["selector"]) {
392
- throw new Error("Invalid search query.");
393
- }
394
- if (!criteria["selector"]["schema"]) {
395
- throw new Error("The search criteria must contain the schema");
573
+ throw new ValidationError("Invalid search query.Use {selector:{...query...}}");
396
574
  }
575
+ //if (!criteria["selector"]["schema"]) {
576
+ // throw new Error("The search criteria must contain the schema");
577
+ //}
397
578
  const results = await this.db_api.search(criteria);
398
579
  return results;
399
580
  }
400
581
 
401
- /**
402
- * Inserts a doc for the given schema
403
- * @param {String} schema e.g "contact"
404
- * @param {Object} data e.g {"name":"","mobile":""...}
405
- * @param {Object} settings (optional)
406
- */
407
- async insert(schema, data, meta= {},settings = {}) {
408
- //console.log("here in insert")
409
- this._check_ready_to_use()
410
- try {
411
- let doc_obj = await this._insert_pre_checks(schema, data, settings);
412
- let new_rec = await this.db_api.insert(doc_obj);
413
- return { id: new_rec["id"] };
414
- } catch (error) {
415
- // console.log(error);
416
- throw error;
417
- }
418
- }
419
-
420
-
421
-
422
-
423
- //** Update data */
424
- /**
425
- * Update data and meta of a doc.
426
- *
427
- * - Q: Which data fields can be edited ?
428
- * - A: Depends on the setting.editable_fields. If this is blank, then all fields are editable.
429
- * - Q: Are primary key fields editable ?
430
- * - A: Yes. before making the update, a check is done to ensue the primary key policy is not violated
431
- *
432
- * @param {String} doc_id
433
- * @param {String} rev_id
434
- * @param {*} schema_name
435
- * @param {doc_obj} updates {data:{},meta:{}}, need not be the full document, just the new values of all/some fields
436
- * @param {Boolean} save_conflict = true -
437
- *
438
- */
439
- async update(doc_id, rev_id, updates, update_source="api",save_conflict = true) {
440
- this._check_ready_to_use()
441
- // making a big assumption here : primary key fields cannot be edited
442
- // so updating the doc will not generate primary key conflicts
443
- let req_data = await this.get(doc_id,true);
444
- let schema = req_data.schema
445
- let full_doc = req_data.doc // await this.get(doc_id)["doc"];
446
-
447
- // @TODO fix this : what to do if the rev id does not match
448
- // if (full_doc["_rev"] != rev_id) {
449
- // // throw error , save conflicting doc separately by default
450
- // if (save_conflict) {
451
- // // save conflicting doc todo
452
- // }
453
- // }
454
-
455
- // update new value depending on settings.editable_fields (if does not exists, all fields are editable)
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))
459
-
460
- // now generate the new doc with updates
461
- let allowed_updates = this._filterObject(updates.data,edit_fields);
462
- let updated_data = { ...full_doc.data, ...allowed_updates };
463
-
464
- this.validate_data(schema.schema, updated_data);
465
-
466
- // primary key check if multiple records can be created
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"}])
477
- }
478
- }
479
- else{
480
- throw new Error("There is something wrong with the schema primary keys")
582
+ /**
583
+ * Retrieves special types of documents from the database, such as schema documents or blank documents
584
+ * for a given schema. It handles system-related data and throws errors for invalid document types
585
+ * or if the document is not found.
586
+ *
587
+ * @param {String} special_doc_type - The type of special document to fetch. Supported types include:
588
+ * - 'schema': Retrieves a schema document based on the criteria provided.
589
+ * @param {Object} [criteria={}] - Criteria used to search for the special document.
590
+ * For example, to search for a schema, the criteria should include the name.
591
+ *
592
+ * @throws {ValidationError} Throws if the `special_doc_type` is not recognized.
593
+ * @throws {DocNotFoundError} Throws if the requested document is not found in the database.
594
+ *
595
+ * @returns {Object} The fetched special document based on the type and criteria.
596
+ */
597
+ async get(special_doc_type,criteria={}){
598
+ // this method returns special types of documents such as schema doc, or a blank doc for a given schema and other system related things
599
+ const fetch_docs = {
600
+ schema:async (criteria)=>{
601
+ let schemaSearch = await this.db_api.search({
602
+ selector: { schema: "schema", "data.name": criteria.name },
603
+ });
604
+ // console.log(schemaSearch)
605
+ if (schemaSearch.docs.length == 0) {
606
+ throw new DocNotFoundError(BeanBagDB.error_codes.schema_not_found);
481
607
  }
482
- }
608
+ return schemaSearch.docs[0];
609
+ }
610
+ }
611
+ if(Object.keys(fetch_docs).includes(special_doc_type)){
612
+ let data = await fetch_docs[special_doc_type](criteria)
613
+ return data
614
+ }else{
615
+ throw new ValidationError("Invalid special doc type. Must be : "+Object.keys(fetch_docs).join(","))
483
616
  }
484
-
485
- // encrypt the data
486
- full_doc["data"] = updated_data
487
- full_doc = this._encrypt_doc(schema,full_doc);
488
-
489
- if(updates.meta){
490
- let m_sch = sys_sch.editable_metadata_schema
491
- let editable_fields = Object.keys(m_sch["properties"])
492
- let allowed_meta = this._filterObject(updates.meta,editable_fields)
493
- this.validate_data(m_sch,allowed_meta)
494
- full_doc["meta"] = {...full_doc["meta"],...allowed_meta}
495
- }
496
-
497
- full_doc.meta["updated_on"] = this._get_now_unix_timestamp()
498
- full_doc.meta["updated_by"] = update_source
499
- let up = await this.db_api.update(full_doc);
500
- return up;
501
- }
502
-
503
- async delete(doc_id) {
504
- this._check_ready_to_use()
505
- await this.db_api.delete(doc_id)
506
617
  }
507
618
 
508
-
509
- async load_plugin(plugin_name,plugin_module){
510
- this._check_ready_to_use()
511
- this.plugins[plugin_name] = {}
512
- for (let func_name in plugin_module){
513
- if(typeof plugin_module[func_name]=='function'){
619
+ async load_plugin(plugin_name, plugin_module) {
620
+ this._check_ready_to_use();
621
+ this.plugins[plugin_name] = {};
622
+ for (let func_name in plugin_module) {
623
+ if (typeof plugin_module[func_name] == "function") {
514
624
  this.plugins[plugin_name][func_name] = plugin_module[func_name].bind(null,this)
515
625
  }
516
626
  }
517
627
  // Check if the plugin has an on_load method and call it
518
- if (typeof this.plugins[plugin_name].on_load === 'function') {
628
+ if (typeof this.plugins[plugin_name].on_load === "function") {
519
629
  await this.plugins[plugin_name].on_load();
520
630
  }
521
631
  }
522
632
 
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
- }
633
+ ///////////////////////////////////////////////////////////
634
+ //////////////// Internal methods ////////////////////////
635
+ //////////////////////////////////////////////////////////
536
636
 
537
- _check_ready_to_use(){
538
- if(!this.active){
539
- throw new Error(this.error_codes.not_active)
637
+ /**
638
+ * Retrieves the current version of the system by summing up the version numbers
639
+ * of all system-defined schemas.
640
+ *
641
+ * @private
642
+ * @returns {number} The total sum of the version numbers of all system-defined schemas.
643
+ *
644
+ * @throws {Error} Throws if there is an issue calculating the sum of version numbers.
645
+ */
646
+ _get_current_version() {
647
+ // current version is the sum of versions of all system defined schemas
648
+ let sum = sys_sch.schema_schema.version;
649
+ let keys = Object.keys(sys_sch.system_schemas).map((item) => {
650
+ sum = sum + sys_sch.system_schemas[item].version;
651
+ });
652
+ if (sum == NaN) {
653
+ throw Error("Error in system schema version numbers");
540
654
  }
655
+ return sum;
541
656
  }
542
657
 
543
-
544
- _generate_random_link(){
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'];
546
- return Array.from({ length: 4 }, () => dictionary[Math.floor(Math.random() * dictionary.length)]).join('-');
547
- }
548
-
549
-
550
-
551
658
  /**
552
- *
553
- * @param {*} obj
554
- * @param {*} fields
555
- *
556
- */
557
- _filterObject(obj, fields) {
558
- return fields.reduce((filteredObj, field) => {
559
- if (Object.prototype.hasOwnProperty.call(obj, field)) {
560
- filteredObj[field] = obj[field];
561
- }
562
- return filteredObj;
563
- }, {});
564
- }
565
-
566
-
567
-
568
- /**
569
- * To update the system schema or reset to a stable version to ensure functioning of the BeanBagDB
570
- */
571
- async _update_system_schema() {
572
- console.log("Todo");
573
- }
574
-
575
- /**
576
- * Returns the current Unix timestamp in seconds.
577
- * divide by 1000 (Date.now gives ms) to convert to seconds. 1 s = 1000 ms
578
- * @returns {number}
659
+ * Checks if the database is ready to use.
660
+ *
661
+ * This method verifies if the database is in an active state. If the database is not ready,
662
+ * it throws an error indicating that the database is inactive.
663
+ *
664
+ * @private
665
+ * @throws {Error} - Throws an error if the database is not active.
579
666
  */
580
- _get_now_unix_timestamp() {
581
- return Math.floor(Date.now() / 1000);
667
+ _check_ready_to_use() {
668
+ if (!this.active) {
669
+ throw new Error(BeanBagDB.error_codes.not_active);
670
+ }
582
671
  }
583
672
 
584
673
  /**
585
674
  * Generates a blank database json object. All objects in the database follow the same structure
675
+ * @private
586
676
  * @param {string} schema_name
587
677
  * @returns {object}
588
678
  */
@@ -593,10 +683,10 @@ export class BeanBagDB {
593
683
  let doc = {
594
684
  data: {},
595
685
  meta: {
596
- created_on: this._get_now_unix_timestamp(),
686
+ created_on: this.util_get_now_unix_timestamp(),
597
687
  tags: [],
598
- app :{},
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
688
+ app: {},
689
+ link: this.util_generate_random_link(), // there is a link by default. overwrite this if user provided one but only before checking if it is unique
600
690
  },
601
691
  schema: schema_name,
602
692
  };
@@ -604,14 +694,15 @@ export class BeanBagDB {
604
694
  }
605
695
 
606
696
  /**
607
- * Generates a blank schema doc ready to be inserted to the database. Note that no validation is done. This is for internal use
697
+ * Generates a blank schema doc ready to be inserted to the database. This is for internal use
698
+ * @private
608
699
  * @param {string} schema_name
609
700
  * @param {Object} schema_object
610
701
  * @param {Object} data
611
702
  * @returns {Object}
612
703
  */
613
704
  _get_blank_schema_doc(schema_name, schema_object, data) {
614
- this.validate_data(schema_object, data);
705
+ this.util_validate_data(schema_object, data);
615
706
  let obj = this._get_blank_doc(schema_name);
616
707
  obj["data"] = data;
617
708
  return obj;
@@ -619,146 +710,357 @@ export class BeanBagDB {
619
710
 
620
711
  /**
621
712
  * Decrypts a given document using it's schema. The list of encrypted fields : schema_obj.settings.encrypted_fields
713
+ * @private
622
714
  * @param {Object} schema_obj
623
715
  * @param {Object} doc_obj
624
716
  * @returns {Object}
625
717
  */
626
- _decrypt_doc(schema_obj, doc_obj) {
627
- if (
628
- schema_obj.settings["encrypted_fields"] &&
629
- schema_obj.settings["encrypted_fields"].length > 0
630
- ) {
631
- schema_obj.settings["encrypted_fields"].forEach((itm) => {
632
- doc_obj.data[itm] = this.utils.decrypt(
633
- doc_obj.data[itm],
634
- this.encryption_key
635
- );
636
- });
718
+ async _decrypt_doc(schema_obj, doc_obj) {
719
+ try {
720
+ for (let itm of schema_obj.settings["encrypted_fields"]) {
721
+ doc_obj.data[itm] = await this.utils.decrypt(doc_obj.data[itm], this.encryption_key);
722
+ }
723
+ return { ...doc_obj };
724
+ } catch (error) {
725
+ console.log(error)
726
+ throw new EncryptionError([{message:error.message}])
637
727
  }
638
- return { ...doc_obj };
728
+
639
729
  }
640
730
 
641
731
  /**
642
732
  * Encrypts a given doc using it's schema obj.
733
+ * @private
643
734
  * @param {Object} schema_obj
644
735
  * @param {Object} doc_obj
645
736
  * @returns {Object}
646
737
  */
647
- _encrypt_doc(schema_obj, doc_obj) {
648
-
649
- if (
650
- schema_obj.settings["encrypted_fields"] &&
651
- schema_obj.settings["encrypted_fields"].length > 0
652
- ) {
653
- // console.log(schema_obj,doc_obj)
654
- schema_obj.settings["encrypted_fields"].forEach((itm) => {
655
- doc_obj.data[itm] = this.utils.encrypt(
656
- doc_obj.data[itm],
657
- this.encryption_key
658
- );
659
- });
738
+ async _encrypt_doc(schema_obj, doc_obj) {
739
+ try {
740
+ if (schema_obj.settings["encrypted_fields"].length > 0) {
741
+ // console.log(schema_obj,doc_obj)
742
+ for (let itm of schema_obj.settings["encrypted_fields"]) {
743
+ doc_obj.data[itm] = await this.utils.encrypt(doc_obj.data[itm], this.encryption_key);
744
+ }
745
+ }
746
+ return { ...doc_obj };
747
+ } catch (error) {
748
+ throw new EncryptionError([{message:error.message}])
660
749
  }
661
- return { ...doc_obj };
662
750
  }
663
751
 
664
752
  /**
665
- * Checks if the new document is valid and ready to be inserted in the DB.
666
- * List of checks:
667
- * - fetch the schema object and validate the data object against the schema
668
- * - check if the doc with same primary keys already exists
669
- * - replace encrypted fields with encrypted values
670
- * - return the doc
671
- * @param {Object} schema
672
- * @param {Object} data
753
+ * Validates the new document before inserting it into the database.
754
+ *
755
+ * This method performs a series of checks:
756
+ * - Fetches the schema object and validates the `data` object against it.
757
+ * - Checks for existing documents with the same primary keys.
758
+ * - Replaces encrypted fields with their encrypted values.
759
+ *
760
+ * It then generates the document ready for insertion and returns it.
761
+ *
762
+ * @private
763
+ * @param {Object} schema - The schema object or schema name to validate against.
764
+ * @param {Object} data - The data object to be validated and prepared for insertion.
765
+ * @param {Object} [meta={}] - Additional metadata related to the document.
766
+ * @param {Object} [settings={}] - Optional settings to guide special checks.
767
+ *
768
+ * @returns {Promise<Object>} - Returns the validated document object ready for insertion.
769
+ *
770
+ * @throws {Error} If validation fails, or the document already exists.
673
771
  */
674
- async _insert_pre_checks(schema, data,meta={} ,settings = {}) {
772
+ async _insert_pre_checks(schema, data, meta = {}, settings = {}) {
675
773
  // schema search
676
- let sch_search = await this.search({
677
- selector: { schema: "schema", "data.name": schema },
678
- });
679
- if (sch_search.docs.length == 0) {
680
- throw new Error("Invalid Schema");
681
- }
774
+ let sch_search = await this.search({selector: { schema: "schema", "data.name": schema }})
775
+ if (sch_search.docs.length == 0) {throw new DocCreationError(`The schema "${schema}" does not exists`)}
682
776
  let schemaDoc = sch_search.docs[0]["data"];
683
777
  // validate data
684
- this.validate_data(schemaDoc.schema, data);
778
+ this.util_validate_data(schemaDoc.schema, data);
685
779
 
686
780
  // validate meta
687
- this.validate_data(sys_sch.editable_metadata_schema, meta);
688
-
781
+ this.util_validate_data(sys_sch.editable_metadata_schema, meta);
782
+
689
783
  // duplicate meta.link check
690
- if(meta.link){
691
- let link_search = await this.search({ selector: {"meta.link":meta.link} });
692
- console.log(link_search);
693
- if (link_search.docs.length > 0) {
694
- throw new Error("This link already exists in the database");
695
- }
784
+ if (meta.link) {
785
+ let link_search = await this.search({selector: { "meta.link": meta.link }})
786
+ if (link_search.docs.length > 0) {throw new DocCreationError(`Document with the link "${meta.link}" already exists in the Database.`)}
696
787
  }
697
788
 
698
789
  // special checks for special docs
699
790
  // @TODO : for schema dos: settings fields must be in schema field
700
- if(schema=="schema"){
791
+ if (schema == "schema") {
701
792
  //more checks are required
702
- this.validate_schema_object(data)
793
+ this.util_validate_schema_object(data);
703
794
  }
704
795
  // @TODO : check if single record setting is set to true
705
-
796
+ //console.log(schemaDoc)
706
797
  // duplicate check
707
- if (
708
- schemaDoc.settings["primary_keys"] &&
709
- schemaDoc.settings["primary_keys"].length > 0
710
- ) {
798
+ if (schemaDoc.settings["primary_keys"].length > 0) {
711
799
  let primary_obj = { schema: schema };
712
- schemaDoc.settings["primary_keys"].map((ky) => {
713
- primary_obj["data." + ky] = data[ky];
714
- });
715
- console.log(primary_obj);
800
+ schemaDoc.settings["primary_keys"].map((ky) => {primary_obj["data." + ky] = data[ky];});
716
801
  let prim_search = await this.search({ selector: primary_obj });
717
- console.log(prim_search);
718
802
  if (prim_search.docs.length > 0) {
719
- throw new Error("Doc already exists");
803
+ throw new DocCreationError(`Document with the given primary key (${schemaDoc.settings["primary_keys"].join(",")}) already exists in the schema "${schema}"`);
720
804
  }
721
805
  }
722
806
  // encrypt if required
723
807
  let new_data = { ...data };
724
- if (
725
- schemaDoc.settings["encrypted_fields"] &&
726
- schemaDoc.settings["encrypted_fields"].length > 0
727
- ) {
728
- schemaDoc.settings["encrypted_fields"].forEach((itm) => {
729
- new_data[itm] = this.utils.encrypt(data[itm], this.encryption_key);
730
- });
808
+ if (schemaDoc.settings["encrypted_fields"].length > 0) {
809
+ // todo test if encryption is successful
810
+ for (let itm of schemaDoc.settings["encrypted_fields"]) {
811
+ new_data[itm] = await this.utils.encrypt(data[itm], this.encryption_key);
812
+ }
731
813
  }
814
+
732
815
  // generate the doc object for data
733
816
  let doc_obj = this._get_blank_doc(schema);
734
817
  doc_obj["data"] = new_data;
818
+ // if meta exists
819
+ if(meta){
820
+ doc_obj["meta"] = {...doc_obj["meta"],...meta};
821
+ }
735
822
  return doc_obj;
736
823
  }
737
824
 
738
- ////// Utility methods
739
- util_check_required_fields(requiredFields,obj){
825
+ ////////////////////////////////////////////////////////////////////////////////////////////////////
826
+ ////// Utility methods /////////////////////////////////////////////////////////////////////////////
827
+ ///////////////////////////////////////////////////////////////////////////////////////////////////
828
+
829
+ /**
830
+ * Returns the current Unix timestamp in seconds.
831
+ * divide by 1000 (Date.now gives ms) to convert to seconds. 1 s = 1000 ms
832
+ * @private
833
+ * @returns {number}
834
+ */
835
+ util_get_now_unix_timestamp() {return Math.floor(Date.now() / 1000)}
836
+
837
+ /**
838
+ * Validates that the required fields are present in the provided object.
839
+ *
840
+ * @param {string[]} requiredFields - An array of field names that are required.
841
+ * @param {object} obj - The object to check for the required fields.
842
+ * @throws {ValidationError} If any of the required fields are missing, an error is thrown.
843
+ */
844
+ util_check_required_fields(requiredFields, obj) {
740
845
  for (const field of requiredFields) {
741
- if (!obj[field]) {throw new Error(`${field} is required`);}
846
+ if (!obj[field]) {throw new ValidationError(`The field ${field} is required.`)}
847
+ }
848
+ }
849
+
850
+ /**
851
+ * Filters an object, returning a new object that only contains the specified fields.
852
+ *
853
+ * @param {Object} obj - The object to filter.
854
+ * @param {Array<String>} fields - An array of field names to retain in the filtered object.
855
+ *
856
+ * @returns {Object} - A new object containing only the fields that exist in `obj` from the `fields` array.
857
+ *
858
+ * **Example**:
859
+ *
860
+ * const data = { name: "Alice", age: 25, location: "NY" };
861
+ * const result = util_filter_object(data, ["name", "age"]);
862
+ * // result: { name: "Alice", age: 25 }
863
+ */
864
+ util_filter_object(obj, fields) {
865
+ return fields.reduce((filteredObj, field) => {
866
+ if (Object.prototype.hasOwnProperty.call(obj, field)) {
867
+ filteredObj[field] = obj[field];
868
+ }
869
+ return filteredObj;
870
+ }, {});
871
+ }
872
+
873
+
874
+ /**
875
+ * Validates a data object against a provided JSON schema
876
+ * It relies on the external API provided by the user
877
+ * @param {Object} schema_obj - The JSON schema object to validate against
878
+ * @param {Object} data_obj - The data object to validate
879
+ * @throws {Error} If the data object does not conform to the schema
880
+ */
881
+ util_validate_data(schema_obj, data_obj) {
882
+ const { valid, validate } = this.utils.validate_schema(schema_obj,data_obj)
883
+ if (!valid) {
884
+ throw new ValidationError(validate.errors);
742
885
  }
743
886
  }
887
+
888
+ /**
889
+ * Validates the structure and content of a schema object.
890
+ *
891
+ * This method checks the following conditions:
892
+ *
893
+ * - The schema must have a 'type' field, which should be 'object'.
894
+ * - The 'properties' field must be an object and contain at least one property.
895
+ * - The 'additionalProperties' field must be present and of type boolean.
896
+ * - Primary keys must be defined in the schema and cannot be of type 'object' or 'array'.
897
+ * - Non-editable fields must be defined in the schema.
898
+ * - Encrypted fields must be defined in the schema, of type 'string', and cannot include primary keys.
899
+ *
900
+ * If any of these conditions are violated, an array of error messages will be
901
+ * collected and thrown as a ValidationError.
902
+ *
903
+ * @param {Object} schema_doc - The schema document to validate.
904
+ * @param {Object} schema_doc.schema - The schema structure containing:
905
+ * @param {String} schema_doc.schema.type - The type of the schema (must be 'object').
906
+ * @param {Object} schema_doc.schema.properties - The properties defined in the schema.
907
+ * @param {Boolean} schema_doc.schema.additionalProperties - Indicates if additional properties are allowed.
908
+ * @param {Object} schema_doc.settings - The settings associated with the schema, including:
909
+ * @param {Array<String>} schema_doc.settings.primary_keys - List of primary keys for the schema.
910
+ * @param {Array<String>} schema_doc.settings.non_editable_fields - Fields that cannot be edited.
911
+ * @param {Array<String>} schema_doc.settings.encrypted_fields - Fields that require encryption.
912
+ *
913
+ * @throws {ValidationError} If any validation checks fail, with an array of error messages.
914
+ */
915
+ util_validate_schema_object(schema_doc) {
916
+ let errors = [{ message: "Schema validation errors " }];
917
+ if (!schema_doc["schema"]["type"]) {
918
+ errors.push({message:"Schema must have the field schema.'type' which can only be 'object' "});
919
+ } else {
920
+ if (schema_doc["schema"]["type"] != "object") {
921
+ errors.push({message: "The schema.'type' value is invalid.Only 'object' allowed",});
922
+ }
923
+ }
924
+ if (!schema_doc["schema"]["properties"]) {
925
+ errors.push({
926
+ message: "The schema.'properties' object does not exists",
927
+ });
928
+ } else {
929
+ if (typeof schema_doc["schema"]["properties"] != "object") {
930
+ errors.push({message:"Invalid schema.properties. It must be an object and must have atleast one field inside.",});
931
+ }
932
+ if (Object.keys(schema_doc["schema"]["properties"]).length == 0) {
933
+ errors.push({ message: "You must define at least one property" });
934
+ }
935
+ }
936
+
937
+ if (!schema_doc["schema"].hasOwnProperty("additionalProperties")) {
938
+ errors.push({message: "The schema.'additionalProperties' field is required",});
939
+ } else {
940
+ if (typeof schema_doc["schema"]["additionalProperties"] != "boolean") {
941
+ errors.push({message:"Invalid schema.additionalProperties. It must be a boolean value",});
942
+ }
943
+ }
944
+
945
+ const allKeys = Object.keys(schema_doc["schema"]["properties"]);
946
+ if (schema_doc["settings"]["primary_keys"].length > 0) {
947
+ // check if all keys belong to the schema and are not of type object
948
+ let all_pk_exist = schema_doc["settings"]["primary_keys"].every(
949
+ (item) =>
950
+ allKeys.includes(item) &&
951
+ schema_doc["schema"]["properties"][item]["type"] != "object" &&
952
+ schema_doc["schema"]["properties"][item]["type"] != "array"
953
+ );
954
+
955
+ if (!all_pk_exist) {
956
+ errors.push({message:"Primary keys invalid. All keys must be defined in the schema and must be non object",});
957
+ }
958
+ }
959
+
960
+ if (schema_doc["settings"]["non_editable_fields"].length > 0) {
961
+ // check if all keys belong to the schema
962
+ let all_ne_exist = schema_doc["settings"]["non_editable_fields"].every(
963
+ (item) => allKeys.includes(item)
964
+ );
965
+ if (!all_ne_exist) {
966
+ errors.push({message:"Non editable fields invalid. All fields must be defined in the schema ",});
967
+ }
968
+ }
969
+
970
+ if (schema_doc["settings"]["encrypted_fields"].length > 0) {
971
+ // check if all keys belong to the schema and are only string
972
+ let all_enc_exist = schema_doc["settings"]["encrypted_fields"].every((item) =>allKeys.includes(item) &&schema_doc["schema"]["properties"][item]["type"] == "string");
973
+ if (!all_enc_exist) {
974
+ errors.push({message:"Invalid encrypted fields. All fields must be defined in the schema and must be string ",});
975
+ }
976
+
977
+ // check : primary keys cannot be encrypted
978
+ let all_enc_no_pk = schema_doc["settings"]["encrypted_fields"].every((item) => !schema_doc["settings"]["primary_keys"].includes(item));
979
+ if (!all_enc_no_pk) {
980
+ errors.push({message:"Invalid encrypted fields.Primary key fields cannot be encrypted ",});
981
+ }
982
+ }
983
+
984
+ /// cannot encrypt primary field keys
985
+ if (errors.length > 1) {throw new ValidationError(errors)}
986
+ }
987
+
988
+ /**
989
+ * Generates a random link composed of four words from a predefined dictionary.
990
+ *
991
+ * The words are selected randomly, and the resulting link is formatted as
992
+ * a hyphen-separated string. This can be useful for creating link for documents.
993
+ *
994
+ * @returns {String} A hyphen-separated string containing three randomly
995
+ * selected words from the dictionary. For example:
996
+ * "banana-earth-rain".
997
+ *
998
+ */
999
+ util_generate_random_link() {
1000
+ // prettier-ignore
1001
+ 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'];
1002
+ return Array.from({ length: 3 },() => dictionary[Math.floor(Math.random() * dictionary.length)]).join("-");
1003
+ }
744
1004
  }
745
1005
 
1006
+ ////////////////// Error classes ////////////////////////////////////////////////////
746
1007
 
1008
+ /**
1009
+ * This is common for all error classes
1010
+ * @typedef {Object} ErrorItem
1011
+ * @property {string} [instancePath] - The path where the error occurred, optional.
1012
+ * @property {string} message - The error message.
1013
+ */
1014
+
1015
+ /**
1016
+ * Custom error class for validation errors.
1017
+ *
1018
+ * @extends {Error}
1019
+ */
747
1020
  export class ValidationError extends Error {
1021
+ /**
1022
+ * Custom error class for validation errors.
1023
+ *
1024
+ * @extends {Error}
1025
+ * @param {ErrorItem[]} [errors=[]] - An array of error objects, each containing details about validation failures.
1026
+ */
748
1027
  constructor(errors = []) {
749
1028
  // Create a message based on the list of errors
750
1029
  //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(",")}`;
1030
+ let error_messages
1031
+ if(Array.isArray(errors)){
1032
+ error_messages = errors.map(item=>` ${(item.instancePath||" ").replace("/","")} ${item.message} `)
1033
+ }else {
1034
+ error_messages = [errors]
1035
+ }
1036
+ let message = `Validation failed with error(s): ${error_messages.join(",")}`;
753
1037
  super(message);
754
1038
  this.name = 'ValidationError';
755
1039
  this.errors = errors; // Store the list of errors
756
1040
  }
757
1041
  }
758
1042
 
1043
+
1044
+
1045
+ /**
1046
+ * Custom error class for document update errors.
1047
+ *
1048
+ * @extends {Error}
1049
+ */
759
1050
  export class DocUpdateError extends Error {
1051
+ /**
1052
+ * Custom error class for document update errors.
1053
+ *
1054
+ * @extends {Error}
1055
+ * @param {ErrorItem[]} [errors=[]] - An array of error objects, each containing details about validation failures.
1056
+ */
760
1057
  constructor(errors=[]){
761
- let error_messages = errors.map(item=>`${item.message}`)
1058
+ let error_messages
1059
+ if(Array.isArray(errors)){
1060
+ error_messages = errors.map(item=>` ${(item.instancePath||" ").replace("/","")} ${item.message} `)
1061
+ }else {
1062
+ error_messages = [errors]
1063
+ }
762
1064
  let message = `Error in document update. ${error_messages.join(",")}`
763
1065
  super(message)
764
1066
  this.name = "DocUpdateError";
@@ -766,22 +1068,81 @@ export class DocUpdateError extends Error {
766
1068
  }
767
1069
  }
768
1070
 
769
- export class DocInsertError extends Error {
1071
+ /**
1072
+ * Custom error class for document insert errors.
1073
+ *
1074
+ * @extends {Error}
1075
+ */
1076
+ export class DocCreationError extends Error {
1077
+ /**
1078
+ * Custom error class for document insert errors.
1079
+ *
1080
+ * @extends {Error}
1081
+ * @param {ErrorItem[]} [errors=[]] - An array of error objects, each containing details about validation failures.
1082
+ */
770
1083
  constructor(errors=[]){
771
- let error_messages = errors.map(item=>`${item.message}`)
772
- let message = `Error in document insert. ${error_messages.join(",")}`
1084
+ let error_messages
1085
+ if(Array.isArray(errors)){
1086
+ error_messages = errors.map(item=>` ${(item.instancePath||" ").replace("/","")} ${item.message} `)
1087
+ }else {
1088
+ error_messages = [errors]
1089
+ }
1090
+ let message = `Error in document creation. ${error_messages.join(",")}`
773
1091
  super(message)
774
- this.name = "DocInsertError";
1092
+ this.name = "DocCreationError";
775
1093
  this.errors = errors
776
1094
  }
777
1095
  }
778
1096
 
1097
+ /**
1098
+ * Custom error class for document not found errors.
1099
+ *
1100
+ * @extends {Error}
1101
+ */
779
1102
  export class DocNotFoundError extends Error {
1103
+ /**
1104
+ * Custom error class for document not found errors.
1105
+ *
1106
+ * @extends {Error}
1107
+ * @param {ErrorItem[]} [errors=[]] - An array of error objects, each containing details about validation failures.
1108
+ */
780
1109
  constructor(errors=[]){
781
- let error_messages = errors.map(item=>`${item.message}`)
1110
+ let error_messages
1111
+ if(Array.isArray(errors)){
1112
+ error_messages = errors.map(item=>` ${(item.instancePath||" ").replace("/","")} ${item.message} `)
1113
+ }else {
1114
+ error_messages = [errors]
1115
+ }
782
1116
  let message = `Error in fetching document. Criteria : ${error_messages.join(",")}`
783
1117
  super(message)
784
1118
  this.name = "DocNotFoundError";
785
1119
  this.errors = errors
786
1120
  }
787
1121
  }
1122
+
1123
+
1124
+ /**
1125
+ * Custom error class for encryption error.
1126
+ *
1127
+ * @extends {Error}
1128
+ */
1129
+ export class EncryptionError extends Error {
1130
+ /**
1131
+ * Custom error class for document not found errors.
1132
+ *
1133
+ * @extends {Error}
1134
+ * @param {ErrorItem[]} [errors=[]] - An array of error objects, each containing details about validation failures.
1135
+ */
1136
+ constructor(errors=[]){
1137
+ let error_messages
1138
+ if(Array.isArray(errors)){
1139
+ error_messages = errors.map(item=>` ${(item.instancePath||" ").replace("/","")} ${item.message} `)
1140
+ }else {
1141
+ error_messages = [errors]
1142
+ }
1143
+ let message = `Error in encryption/decryption of data : ${error_messages.join(",")}`
1144
+ super(message)
1145
+ this.name = "EncryptionError";
1146
+ this.errors = errors
1147
+ }
1148
+ }