beanbagdb 0.5.51 → 0.5.52
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/README.md +7 -129
- package/doc-src/0_about.md +115 -0
- package/doc-src/1_getting-started.md +1 -0
- package/doc-src/2_schema.md +1 -0
- package/doc-src/3_plugins.md +1 -0
- package/doc-src/contents.json +15 -0
- package/jsdoc.json +1 -1
- package/package.json +3 -2
- package/src/index.js +804 -437
- package/src/system_schema.js +1 -1
- package/test/operations.test.js +1 -1
- package/test/test1.js +5 -14
- /package/{docs → doc-src}/logo.png +0 -0
package/src/index.js
CHANGED
|
@@ -1,205 +1,307 @@
|
|
|
1
1
|
import * as sys_sch from "./system_schema.js";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* -
|
|
17
|
-
*
|
|
18
|
-
* -
|
|
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
|
-
|
|
22
|
-
this.util_check_required_fields(["
|
|
23
|
-
this.util_check_required_fields(["
|
|
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){
|
|
27
|
-
|
|
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
|
|
36
|
-
backend_database
|
|
37
|
-
beanbagdb_version_db
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
this.
|
|
41
|
-
|
|
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
|
-
|
|
55
|
-
|
|
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
|
-
...
|
|
58
|
-
beanbagdb_version_code
|
|
59
|
-
ready_to_use
|
|
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
|
-
*
|
|
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
|
|
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.
|
|
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
|
|
89
|
-
*
|
|
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
|
|
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
|
|
98
|
-
if (schema["data"]["version"] != sys_sch.schema_schema.version){
|
|
99
|
-
logs.push("old schema_schema v "+schema["data"]["version"])
|
|
100
|
-
let full_doc
|
|
101
|
-
full_doc["data"] =
|
|
102
|
-
full_doc["meta"]["updated_on"] = this.
|
|
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("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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
172
|
+
console.log(error);
|
|
173
|
+
if (error instanceof DocNotFoundError) {
|
|
174
|
+
console.log("iiii")
|
|
175
|
+
//error.message == BeanBagDB.error_codes.schema_not_found) {
|
|
176
|
+
console.log("...adding new ");
|
|
111
177
|
// inserting new schema_schema doc
|
|
112
178
|
let schema_schema_doc = this._get_blank_doc("schema");
|
|
113
179
|
schema_schema_doc.data = sys_sch.schema_schema;
|
|
114
180
|
await this.db_api.insert(schema_schema_doc);
|
|
115
|
-
logs.push("init schema_schema v "+sys_sch.schema_schema.version)
|
|
116
|
-
|
|
181
|
+
logs.push("init schema_schema v " + sys_sch.schema_schema.version);
|
|
182
|
+
}
|
|
117
183
|
}
|
|
118
184
|
|
|
119
185
|
let keys = Object.keys(sys_sch.system_schemas);
|
|
120
186
|
for (let index = 0; index < keys.length; index++) {
|
|
121
|
-
const schema_name = sys_sch.system_schemas[keys[index]]["name"]
|
|
187
|
+
const schema_name = sys_sch.system_schemas[keys[index]]["name"];
|
|
122
188
|
const schema_data = sys_sch.system_schemas[keys[index]];
|
|
123
189
|
try {
|
|
124
190
|
// console.log(schema_name)
|
|
125
|
-
let schema1 = await
|
|
126
|
-
if (schema1["data"]["version"] != schema_data.version){
|
|
127
|
-
logs.push("old "+schema_name+" v "+schema1["data"]["version"])
|
|
128
|
-
let full_doc
|
|
129
|
-
full_doc["data"] =
|
|
130
|
-
full_doc["meta"]["updated_on"] = this.
|
|
131
|
-
await this.db_api.update(full_doc)
|
|
132
|
-
logs.push("new "+schema_name+" v "+schema_data.version)
|
|
191
|
+
let schema1 = await this.get_schema(schema_name);
|
|
192
|
+
if (schema1["data"]["version"] != schema_data.version) {
|
|
193
|
+
logs.push("old " + schema_name + " v " + schema1["data"]["version"]);
|
|
194
|
+
let full_doc = await this.db_api.get(schema1["_id"]);
|
|
195
|
+
full_doc["data"] = { ...schema_data };
|
|
196
|
+
full_doc["meta"]["updated_on"] = this.util_get_now_unix_timestamp();
|
|
197
|
+
await this.db_api.update(full_doc);
|
|
198
|
+
logs.push("new " + schema_name + " v " + schema_data.version);
|
|
133
199
|
}
|
|
134
200
|
} catch (error) {
|
|
135
|
-
console.log(error)
|
|
136
|
-
if (error
|
|
201
|
+
console.log(error);
|
|
202
|
+
if (error instanceof DocNotFoundError) {
|
|
137
203
|
// inserting new schema doc
|
|
138
|
-
let new_schema_doc = this._get_blank_schema_doc(
|
|
204
|
+
let new_schema_doc = this._get_blank_schema_doc(
|
|
205
|
+
"schema",
|
|
206
|
+
sys_sch.schema_schema["schema"],
|
|
207
|
+
schema_data
|
|
208
|
+
);
|
|
139
209
|
await this.db_api.insert(new_schema_doc);
|
|
140
|
-
logs.push("init "+schema_name+" v "+schema_data.version)
|
|
210
|
+
logs.push("init " + schema_name + " v " + schema_data.version);
|
|
141
211
|
}
|
|
142
212
|
}
|
|
143
213
|
}
|
|
144
|
-
// store the logs in the log_doc , generate it for the first time
|
|
214
|
+
// store the logs in the log_doc , generate it for the first time
|
|
145
215
|
// console.log(logs)
|
|
146
|
-
if(logs.length>1){
|
|
216
|
+
if (logs.length > 1) {
|
|
147
217
|
// 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.
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
this.
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
218
|
+
logs.push("Init done");
|
|
219
|
+
|
|
220
|
+
await this.save_setting_doc("system_logs", {
|
|
221
|
+
value: { text: logs.join(","), added: this.util_get_now_unix_timestamp() },
|
|
222
|
+
on_update_array: "append",
|
|
223
|
+
});
|
|
224
|
+
await this.save_setting_doc("beanbagdb_version", {
|
|
225
|
+
value: this._version,
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
this.meta.beanbagdb_version_db = this._version;
|
|
229
|
+
this.active = true;
|
|
230
|
+
} else {
|
|
231
|
+
// no new updates were done
|
|
232
|
+
console.log("Database already up to date");
|
|
159
233
|
}
|
|
160
234
|
}
|
|
161
|
-
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Updates a setting document if it already exists in the database or creates a new document
|
|
239
|
+
* Inserts or updates a setting in the system settings schema.
|
|
240
|
+
*
|
|
241
|
+
* This method either:
|
|
242
|
+
* - Updates an existing document if the setting with the given `name` already exists in the database.
|
|
243
|
+
* - Inserts a new document if no matching setting is found.
|
|
244
|
+
*
|
|
245
|
+
* If the setting exists and the `value` is an array, the behavior depends on the `on_update_array` key:
|
|
246
|
+
* - `"append"`: Appends the new value to the existing array.
|
|
247
|
+
* - `"update"`: Replaces the current array with the new value.
|
|
248
|
+
*
|
|
249
|
+
* @async
|
|
250
|
+
* @param {string} name - The name of the setting to insert or update.
|
|
251
|
+
* @param {object} new_data - The new data to insert or update.
|
|
252
|
+
* @param {*} new_data.value - The value to insert or update.
|
|
253
|
+
* @param {string} [new_data.on_update_array] - Optional behavior for handling arrays, either "append" or "update".
|
|
254
|
+
* @param {object} [schema={}] - Optional schema to validate the data against (currently not implemented).
|
|
255
|
+
* @returns {Promise<object>} - The updated or newly inserted document.
|
|
256
|
+
* @throws {Error} - Throws an error if `new_data` or `new_data.value` is not provided, or if `on_update_array` is invalid.
|
|
257
|
+
*/
|
|
258
|
+
async save_setting_doc(name, new_data, schema = {}) {
|
|
162
259
|
// TODO implement schema check
|
|
163
|
-
if(!new_data){
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
260
|
+
if (!new_data) {
|
|
261
|
+
throw new Error("No data provided");
|
|
262
|
+
}
|
|
263
|
+
if (!new_data.value) {
|
|
264
|
+
throw new Error("No value provided");
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
let doc_search = await this.db_api.search({
|
|
268
|
+
selector: { schema: "system_settings", "data.name": name },
|
|
269
|
+
});
|
|
270
|
+
if (doc_search.docs.length > 0) {
|
|
271
|
+
// doc already exists, check schema and update it : if it exists then it's value already exists and can be
|
|
272
|
+
let doc = { ...doc_search.docs[0] };
|
|
273
|
+
if (Array.isArray(doc.data.value)) {
|
|
274
|
+
let append_type = doc.data.on_update_array;
|
|
275
|
+
if (append_type == "append") {
|
|
276
|
+
doc["data"]["value"].push(new_data.value);
|
|
277
|
+
} else if (append_type == "update") {
|
|
278
|
+
doc["data"]["value"] = new_data.value;
|
|
279
|
+
} else {
|
|
280
|
+
throw new Error("Invalid on update array value");
|
|
181
281
|
}
|
|
182
|
-
|
|
183
|
-
doc["
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
282
|
+
} else {
|
|
283
|
+
doc["data"]["value"] = new_data.value;
|
|
284
|
+
}
|
|
285
|
+
// finally update it
|
|
286
|
+
doc["meta"]["updated_on"] = this.util_get_now_unix_timestamp();
|
|
287
|
+
await this.db_api.update(doc);
|
|
288
|
+
return doc;
|
|
289
|
+
} else {
|
|
290
|
+
// doc does not exists, generate a new one
|
|
291
|
+
let new_val = { value: new_data.value };
|
|
292
|
+
|
|
293
|
+
if (new_data.on_update_array) {
|
|
192
294
|
// 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
|
|
295
|
+
new_val.value = [new_data.value];
|
|
296
|
+
new_val.on_update_array = new_data.on_update_array;
|
|
195
297
|
}
|
|
196
|
-
let new_doc = this._get_blank_doc("system_settings")
|
|
298
|
+
let new_doc = this._get_blank_doc("system_settings");
|
|
197
299
|
new_doc["data"] = {
|
|
198
|
-
|
|
199
|
-
...new_val
|
|
200
|
-
}
|
|
201
|
-
let d = await this.db_api.insert(new_doc)
|
|
202
|
-
return d
|
|
300
|
+
name: name,
|
|
301
|
+
...new_val,
|
|
302
|
+
};
|
|
303
|
+
let d = await this.db_api.insert(new_doc);
|
|
304
|
+
return d;
|
|
203
305
|
}
|
|
204
306
|
}
|
|
205
307
|
|
|
@@ -208,7 +310,7 @@ export class BeanBagDB {
|
|
|
208
310
|
* 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
311
|
*/
|
|
210
312
|
async update_indexes() {
|
|
211
|
-
this._check_ready_to_use()
|
|
313
|
+
this._check_ready_to_use();
|
|
212
314
|
// @TODO check this. i don't the index created this way are actually useful in search.
|
|
213
315
|
let all_schemas_docs = await this.db_api.search({
|
|
214
316
|
selector: { schema: "schema" },
|
|
@@ -222,121 +324,243 @@ export class BeanBagDB {
|
|
|
222
324
|
await this.db_api.createIndex({ index: { fields: indexes } });
|
|
223
325
|
}
|
|
224
326
|
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
////////////////////////////////////////////////////////
|
|
330
|
+
/////////////// CRUD operations /////////////////////
|
|
331
|
+
///////////////////////////////////////////////////////
|
|
332
|
+
|
|
225
333
|
/**
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
* @
|
|
334
|
+
* Creates a document for the given schema into the database.
|
|
335
|
+
*
|
|
336
|
+
* This method validates the input data and schema before inserting a new document into the database.
|
|
337
|
+
*
|
|
338
|
+
* @async
|
|
339
|
+
* @param {string} schema - The schema name for the document, e.g., "contact".
|
|
340
|
+
* @param {object} data - The document data, e.g., { "name": "", "mobile": "", ... }.
|
|
341
|
+
* @param {object} [meta={}] - Optional metadata associated with the document.
|
|
342
|
+
* @param {object} [settings={}] - Optional settings that may affect document creation behavior.
|
|
343
|
+
* @returns {Promise<{id: string}>} - A promise that resolves with the newly inserted document's ID.
|
|
344
|
+
* @throws {Error} - Throws an error if insertion checks fail or if there is an issue with the database operation.
|
|
231
345
|
*/
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
346
|
+
async create(schema, data, meta = {}, settings = {}) {
|
|
347
|
+
this._check_ready_to_use();
|
|
348
|
+
try {
|
|
349
|
+
let doc_obj = await this._insert_pre_checks(schema, data, settings);
|
|
350
|
+
let new_rec = await this.db_api.insert(doc_obj);
|
|
351
|
+
return { id: new_rec["id"] };
|
|
352
|
+
} catch (error) {
|
|
353
|
+
throw error;
|
|
239
354
|
}
|
|
240
355
|
}
|
|
241
356
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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: none };
|
|
384
|
+
if (criteria._id) {
|
|
385
|
+
let doc = await this.db_api.get(criteria._id);
|
|
386
|
+
obj.doc = doc;
|
|
387
|
+
} else if (criteria.link) {
|
|
388
|
+
let linkSearch = await this.db_api.search({selector: { "meta.link": criteria.link },});
|
|
389
|
+
if (linkSearch.docs.length == 0) {throw new DocNotFoundError(BeanBagDB.error_codes.doc_not_found);}
|
|
390
|
+
obj.doc = linkSearch.docs[0];
|
|
391
|
+
} else if (criteria.schema & criteria.data) {
|
|
392
|
+
let pkSearch = await this.db_api.search({selector: { "schema": criteria.schema, "data":criteria.data },});
|
|
393
|
+
if (pkSearch.docs.length == 0) {throw new DocNotFoundError(BeanBagDB.error_codes.doc_not_found);}
|
|
394
|
+
obj.doc = pkSearch.docs[0];
|
|
395
|
+
} else {
|
|
396
|
+
throw new ValidationError(`Invalid criteria to read a document. Valid ways : {"schema":"schema_name","data":{...primary key}} or {"_id":""} or {"link":""} `);
|
|
260
397
|
}
|
|
261
398
|
|
|
262
|
-
if(
|
|
263
|
-
|
|
264
|
-
}else{
|
|
265
|
-
if(typeof(schema_doc["schema"]["additionalProperties"])!="boolean"){
|
|
266
|
-
errors.push({message:"Invalid schema.additionalProperties. It must be a boolean value"})
|
|
267
|
-
}
|
|
399
|
+
if (include_schema) {
|
|
400
|
+
obj.schema = await this.get_schema(obj.doc.schema);
|
|
268
401
|
}
|
|
402
|
+
return obj;
|
|
403
|
+
}
|
|
269
404
|
|
|
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
405
|
|
|
406
|
+
/**
|
|
407
|
+
* Updates the data and metadata of a document.
|
|
408
|
+
*
|
|
409
|
+
* **Frequently Asked Questions**:
|
|
410
|
+
*
|
|
411
|
+
* - **Which data fields can be edited?**
|
|
412
|
+
* - 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.
|
|
413
|
+
*
|
|
414
|
+
* - **Are primary key fields editable?**
|
|
415
|
+
* - Yes, but a validation check ensures that primary key policies are not violated before the update is applied.
|
|
416
|
+
*
|
|
417
|
+
*
|
|
418
|
+
* @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}}).
|
|
419
|
+
* @param {String} rev_id - The document's revision ID (`_rev`) used for version control and conflict detection.
|
|
420
|
+
* @param {Object} updates - The updated values for the document, structured as `{data: {}, meta: {}}`. Only the fields to be updated need to be provided.
|
|
421
|
+
* @param {String} [update_source="api"] - Identifies the source of the update (default: "api").
|
|
422
|
+
* @param {Boolean} [save_conflict=true] - If `true`, conflicting updates will be saved separately in case of revision mismatches.
|
|
423
|
+
*
|
|
424
|
+
* **Behavior**:
|
|
425
|
+
* - Retrieves the document based on the provided search criteria.
|
|
426
|
+
* - Checks the revision ID to detect potential conflicts. (To be implemented: behavior when the `rev_id` does not match).
|
|
427
|
+
* - Validates editable fields against `schema.settings.editable_fields` (or allows editing of all fields if not specified).
|
|
428
|
+
* - Performs primary key conflict checks if multiple records are allowed (`single_record == false`).
|
|
429
|
+
* - Encrypts fields if encryption is required by the schema settings.
|
|
430
|
+
* - Updates the `meta` fields (such as `updated_on` and `updated_by`) and saves the updated document to the database.
|
|
431
|
+
*
|
|
432
|
+
* **Returns**:
|
|
433
|
+
* @returns {Object} The result of the document update operation.
|
|
434
|
+
*
|
|
435
|
+
* **Errors**:
|
|
436
|
+
* - Throws an error if a document with the same primary keys already exists (and `single_record == false`).
|
|
437
|
+
* - Throws a `DocUpdateError` if a primary key conflict is detected during the update.
|
|
438
|
+
*
|
|
439
|
+
* @throws {DocUpdateError} - If a document with conflicting primary keys already exists.
|
|
440
|
+
* @throws {ValidationError} - If the provided data or metadata is invalid according to the schema.
|
|
441
|
+
*/
|
|
442
|
+
async update(
|
|
443
|
+
doc_search_criteria,
|
|
444
|
+
rev_id,
|
|
445
|
+
updates,
|
|
446
|
+
update_source = "api",
|
|
447
|
+
save_conflict = true
|
|
448
|
+
) {
|
|
449
|
+
this._check_ready_to_use();
|
|
450
|
+
// making a big assumption here : primary key fields cannot be edited
|
|
451
|
+
// so updating the doc will not generate primary key conflicts
|
|
452
|
+
let req_data = await this.read(doc_search_criteria, true);
|
|
453
|
+
let schema = req_data.schema;
|
|
454
|
+
let full_doc = req_data.doc;
|
|
455
|
+
|
|
456
|
+
// @TODO fix this : what to do if the rev id does not match
|
|
457
|
+
// if (full_doc["_rev"] != rev_id) {
|
|
458
|
+
// // throw error , save conflicting doc separately by default
|
|
459
|
+
// if (save_conflict) {
|
|
460
|
+
// // save conflicting doc todo
|
|
461
|
+
// }
|
|
462
|
+
// }
|
|
280
463
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
}
|
|
464
|
+
// update new value depending on settings.non_editable_fields (if does not exists, all fields are editable)
|
|
465
|
+
let all_fields = Object.keys(schema.schema.properties);
|
|
466
|
+
let unedit_fields = schema.settings["non_editable_fields"];
|
|
467
|
+
let edit_fields = all_fields.filter(
|
|
468
|
+
(item) => !unedit_fields.includes(item)
|
|
469
|
+
);
|
|
288
470
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
-
}
|
|
471
|
+
// now generate the new doc with updates
|
|
472
|
+
let allowed_updates = this.util_filter_object(updates.data, edit_fields);
|
|
473
|
+
let updated_data = { ...full_doc.data, ...allowed_updates };
|
|
295
474
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
475
|
+
this.util_validate_data(schema.schema, updated_data);
|
|
476
|
+
|
|
477
|
+
// primary key check if multiple records can be created
|
|
478
|
+
if ( schema.settings["single_record"] == false && schema.settings["primary_keys"].length > 0) {
|
|
479
|
+
let pri_fields = schema.settings["primary_keys"];
|
|
480
|
+
let search_criteria = { schema: schema.name };
|
|
481
|
+
pri_fields.map((itm) => {search_criteria["data." + itm] = updated_data[itm];});
|
|
482
|
+
let search = await this.search({ selection: search_criteria });
|
|
483
|
+
if (search.docs.length > 0) {
|
|
484
|
+
if (search.docs.length == 1) {
|
|
485
|
+
let thedoc = search.docs[0];
|
|
486
|
+
if (thedoc["_id"] != doc_id) {
|
|
487
|
+
throw new DocUpdateError([{message:"Update not allowed. Document with the same primary key already exists",}]);
|
|
488
|
+
}
|
|
489
|
+
} else {
|
|
490
|
+
throw new Error("There is something wrong with the schema primary keys");
|
|
491
|
+
}
|
|
300
492
|
}
|
|
301
493
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
494
|
+
|
|
495
|
+
// encrypt the data
|
|
496
|
+
full_doc["data"] = updated_data;
|
|
497
|
+
full_doc = this._encrypt_doc(schema, full_doc);
|
|
498
|
+
|
|
499
|
+
if (updates.meta) {
|
|
500
|
+
let m_sch = sys_sch.editable_metadata_schema;
|
|
501
|
+
let editable_fields = Object.keys(m_sch["properties"]);
|
|
502
|
+
let allowed_meta = this.util_filter_object(updates.meta, editable_fields);
|
|
503
|
+
this.util_validate_data(m_sch, allowed_meta);
|
|
504
|
+
full_doc["meta"] = { ...full_doc["meta"], ...allowed_meta };
|
|
306
505
|
}
|
|
506
|
+
|
|
507
|
+
full_doc.meta["updated_on"] = this.util_get_now_unix_timestamp();
|
|
508
|
+
full_doc.meta["updated_by"] = update_source;
|
|
509
|
+
let up = await this.db_api.update(full_doc);
|
|
510
|
+
return up;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Deletes a document from the database by its ID.
|
|
516
|
+
*
|
|
517
|
+
* @param {String} doc_id - The ID of the document to delete.
|
|
518
|
+
* @throws {DocNotFoundError} If the document with the specified ID does not exist.
|
|
519
|
+
* @throws {ValidationError} If the database is not ready to use.
|
|
520
|
+
*/
|
|
521
|
+
async delete(doc_id) {
|
|
522
|
+
this._check_ready_to_use();
|
|
523
|
+
await this.db_api.delete(doc_id);
|
|
307
524
|
}
|
|
308
525
|
|
|
526
|
+
|
|
527
|
+
////////////////////////////////////////////////////////
|
|
528
|
+
////////////////// Search ////////////////////////////
|
|
529
|
+
///////////////////////////////////////////////////////
|
|
530
|
+
|
|
531
|
+
|
|
309
532
|
/**
|
|
310
|
-
*
|
|
311
|
-
*
|
|
312
|
-
*
|
|
313
|
-
* @
|
|
533
|
+
* Searches for documents in the database for the specified query. The query are Mango queries.
|
|
534
|
+
* One field is mandatory : Schema
|
|
535
|
+
* E.g
|
|
536
|
+
* @param {Object} criteria
|
|
314
537
|
*/
|
|
315
|
-
async
|
|
316
|
-
this._check_ready_to_use()
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
if(
|
|
321
|
-
|
|
322
|
-
}
|
|
323
|
-
|
|
538
|
+
async search(criteria) {
|
|
539
|
+
this._check_ready_to_use();
|
|
540
|
+
if (!criteria["selector"]) {
|
|
541
|
+
throw new Error("Invalid search query.");
|
|
542
|
+
}
|
|
543
|
+
if (!criteria["selector"]["schema"]) {
|
|
544
|
+
throw new Error("The search criteria must contain the schema");
|
|
545
|
+
}
|
|
546
|
+
const results = await this.db_api.search(criteria);
|
|
547
|
+
return results;
|
|
324
548
|
}
|
|
325
549
|
|
|
326
|
-
|
|
327
|
-
* Returns schema document for the given schema name
|
|
550
|
+
/**
|
|
551
|
+
* Returns schema document for the given schema name
|
|
328
552
|
* @param {String} schema_name - Schema name
|
|
329
553
|
*/
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
554
|
+
async get_schema(schema_name) {
|
|
555
|
+
let schemaSearch = await this.db_api.search({
|
|
556
|
+
selector: { schema: "schema", "data.name": schema_name },
|
|
557
|
+
});
|
|
558
|
+
// console.log(schemaSearch)
|
|
559
|
+
if (schemaSearch.docs.length == 0) {
|
|
560
|
+
throw new DocNotFoundError([{message:BeanBagDB.error_codes.schema_not_found}]);
|
|
561
|
+
}
|
|
562
|
+
return schemaSearch.docs[0];
|
|
337
563
|
}
|
|
338
|
-
return schemaSearch.docs[0];
|
|
339
|
-
}
|
|
340
564
|
|
|
341
565
|
/**
|
|
342
566
|
* Fetches a document based on a given schema and primary key.
|
|
@@ -347,8 +571,8 @@ export class BeanBagDB {
|
|
|
347
571
|
* @returns object
|
|
348
572
|
*/
|
|
349
573
|
async get_doc(schema_name, primary_key = {}) {
|
|
350
|
-
this._check_ready_to_use()
|
|
351
|
-
let schema_doc = await this.
|
|
574
|
+
this._check_ready_to_use();
|
|
575
|
+
let schema_doc = await this.get_schema(schema_name);
|
|
352
576
|
let s_doc = schema_doc["data"];
|
|
353
577
|
let doc_obj;
|
|
354
578
|
if (
|
|
@@ -380,209 +604,62 @@ export class BeanBagDB {
|
|
|
380
604
|
return doc_obj;
|
|
381
605
|
}
|
|
382
606
|
|
|
383
|
-
/**
|
|
384
|
-
* Searches for documents in the database for the specified query. The query are Mango queries.
|
|
385
|
-
* One field is mandatory : Schema
|
|
386
|
-
* E.g
|
|
387
|
-
* @param {Object} criteria
|
|
388
|
-
*/
|
|
389
|
-
async search(criteria) {
|
|
390
|
-
this._check_ready_to_use()
|
|
391
|
-
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");
|
|
396
|
-
}
|
|
397
|
-
const results = await this.db_api.search(criteria);
|
|
398
|
-
return results;
|
|
399
|
-
}
|
|
400
|
-
|
|
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")
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
}
|
|
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
|
-
}
|
|
507
|
-
|
|
508
607
|
|
|
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]==
|
|
514
|
-
this.plugins[plugin_name][func_name] = plugin_module[func_name].bind(
|
|
608
|
+
async load_plugin(plugin_name, plugin_module) {
|
|
609
|
+
this._check_ready_to_use();
|
|
610
|
+
this.plugins[plugin_name] = {};
|
|
611
|
+
for (let func_name in plugin_module) {
|
|
612
|
+
if (typeof plugin_module[func_name] == "function") {
|
|
613
|
+
this.plugins[plugin_name][func_name] = plugin_module[func_name].bind(
|
|
614
|
+
null,
|
|
615
|
+
this
|
|
616
|
+
);
|
|
515
617
|
}
|
|
516
618
|
}
|
|
517
619
|
// Check if the plugin has an on_load method and call it
|
|
518
|
-
if (typeof this.plugins[plugin_name].on_load ===
|
|
620
|
+
if (typeof this.plugins[plugin_name].on_load === "function") {
|
|
519
621
|
await this.plugins[plugin_name].on_load();
|
|
520
622
|
}
|
|
521
623
|
}
|
|
522
624
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
625
|
+
///////////////////////////////////////////////////////////
|
|
626
|
+
//////////////// Internal methods ////////////////////////
|
|
627
|
+
//////////////////////////////////////////////////////////
|
|
567
628
|
|
|
568
629
|
/**
|
|
569
|
-
*
|
|
630
|
+
* @private
|
|
631
|
+
* @returns {number}
|
|
570
632
|
*/
|
|
571
|
-
|
|
572
|
-
|
|
633
|
+
_get_current_version() {
|
|
634
|
+
// current version is the sum of versions of all system defined schemas
|
|
635
|
+
let sum = sys_sch.schema_schema.version;
|
|
636
|
+
let keys = Object.keys(sys_sch.system_schemas).map((item) => {
|
|
637
|
+
sum = sum + sys_sch.system_schemas[item].version;
|
|
638
|
+
});
|
|
639
|
+
if (sum == NaN) {
|
|
640
|
+
throw Error("Error in system schema version numbers");
|
|
641
|
+
}
|
|
642
|
+
return sum;
|
|
573
643
|
}
|
|
574
644
|
|
|
575
645
|
/**
|
|
576
|
-
*
|
|
577
|
-
*
|
|
578
|
-
*
|
|
646
|
+
* Checks if the database is ready to use.
|
|
647
|
+
*
|
|
648
|
+
* This method verifies if the database is in an active state. If the database is not ready,
|
|
649
|
+
* it throws an error indicating that the database is inactive.
|
|
650
|
+
*
|
|
651
|
+
* @private
|
|
652
|
+
* @throws {Error} - Throws an error if the database is not active.
|
|
579
653
|
*/
|
|
580
|
-
|
|
581
|
-
|
|
654
|
+
_check_ready_to_use() {
|
|
655
|
+
if (!this.active) {
|
|
656
|
+
throw new Error(BeanBagDB.error_codes.not_active);
|
|
657
|
+
}
|
|
582
658
|
}
|
|
583
659
|
|
|
584
660
|
/**
|
|
585
661
|
* Generates a blank database json object. All objects in the database follow the same structure
|
|
662
|
+
* @private
|
|
586
663
|
* @param {string} schema_name
|
|
587
664
|
* @returns {object}
|
|
588
665
|
*/
|
|
@@ -593,10 +670,10 @@ export class BeanBagDB {
|
|
|
593
670
|
let doc = {
|
|
594
671
|
data: {},
|
|
595
672
|
meta: {
|
|
596
|
-
created_on: this.
|
|
673
|
+
created_on: this.util_get_now_unix_timestamp(),
|
|
597
674
|
tags: [],
|
|
598
|
-
app
|
|
599
|
-
link
|
|
675
|
+
app: {},
|
|
676
|
+
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
677
|
},
|
|
601
678
|
schema: schema_name,
|
|
602
679
|
};
|
|
@@ -605,13 +682,14 @@ export class BeanBagDB {
|
|
|
605
682
|
|
|
606
683
|
/**
|
|
607
684
|
* Generates a blank schema doc ready to be inserted to the database. Note that no validation is done. This is for internal use
|
|
685
|
+
* @private
|
|
608
686
|
* @param {string} schema_name
|
|
609
687
|
* @param {Object} schema_object
|
|
610
688
|
* @param {Object} data
|
|
611
689
|
* @returns {Object}
|
|
612
690
|
*/
|
|
613
691
|
_get_blank_schema_doc(schema_name, schema_object, data) {
|
|
614
|
-
this.
|
|
692
|
+
this.util_validate_data(schema_object, data);
|
|
615
693
|
let obj = this._get_blank_doc(schema_name);
|
|
616
694
|
obj["data"] = data;
|
|
617
695
|
return obj;
|
|
@@ -619,6 +697,7 @@ export class BeanBagDB {
|
|
|
619
697
|
|
|
620
698
|
/**
|
|
621
699
|
* Decrypts a given document using it's schema. The list of encrypted fields : schema_obj.settings.encrypted_fields
|
|
700
|
+
* @private
|
|
622
701
|
* @param {Object} schema_obj
|
|
623
702
|
* @param {Object} doc_obj
|
|
624
703
|
* @returns {Object}
|
|
@@ -640,12 +719,12 @@ export class BeanBagDB {
|
|
|
640
719
|
|
|
641
720
|
/**
|
|
642
721
|
* Encrypts a given doc using it's schema obj.
|
|
722
|
+
* @private
|
|
643
723
|
* @param {Object} schema_obj
|
|
644
724
|
* @param {Object} doc_obj
|
|
645
725
|
* @returns {Object}
|
|
646
726
|
*/
|
|
647
727
|
_encrypt_doc(schema_obj, doc_obj) {
|
|
648
|
-
|
|
649
728
|
if (
|
|
650
729
|
schema_obj.settings["encrypted_fields"] &&
|
|
651
730
|
schema_obj.settings["encrypted_fields"].length > 0
|
|
@@ -662,16 +741,26 @@ export class BeanBagDB {
|
|
|
662
741
|
}
|
|
663
742
|
|
|
664
743
|
/**
|
|
665
|
-
*
|
|
666
|
-
*
|
|
667
|
-
*
|
|
668
|
-
* -
|
|
669
|
-
* -
|
|
670
|
-
* -
|
|
671
|
-
*
|
|
672
|
-
*
|
|
744
|
+
* Validates the new document before inserting it into the database.
|
|
745
|
+
*
|
|
746
|
+
* This method performs a series of checks:
|
|
747
|
+
* - Fetches the schema object and validates the `data` object against it.
|
|
748
|
+
* - Checks for existing documents with the same primary keys.
|
|
749
|
+
* - Replaces encrypted fields with their encrypted values.
|
|
750
|
+
*
|
|
751
|
+
* It then generates the document ready for insertion and returns it.
|
|
752
|
+
*
|
|
753
|
+
* @private
|
|
754
|
+
* @param {Object} schema - The schema object or schema name to validate against.
|
|
755
|
+
* @param {Object} data - The data object to be validated and prepared for insertion.
|
|
756
|
+
* @param {Object} [meta={}] - Additional metadata related to the document.
|
|
757
|
+
* @param {Object} [settings={}] - Optional settings to guide special checks.
|
|
758
|
+
*
|
|
759
|
+
* @returns {Promise<Object>} - Returns the validated document object ready for insertion.
|
|
760
|
+
*
|
|
761
|
+
* @throws {Error} If validation fails, or the document already exists.
|
|
673
762
|
*/
|
|
674
|
-
async _insert_pre_checks(schema, data,meta={}
|
|
763
|
+
async _insert_pre_checks(schema, data, meta = {}, settings = {}) {
|
|
675
764
|
// schema search
|
|
676
765
|
let sch_search = await this.search({
|
|
677
766
|
selector: { schema: "schema", "data.name": schema },
|
|
@@ -681,14 +770,16 @@ export class BeanBagDB {
|
|
|
681
770
|
}
|
|
682
771
|
let schemaDoc = sch_search.docs[0]["data"];
|
|
683
772
|
// validate data
|
|
684
|
-
this.
|
|
773
|
+
this.util_validate_data(schemaDoc.schema, data);
|
|
685
774
|
|
|
686
775
|
// validate meta
|
|
687
|
-
this.
|
|
688
|
-
|
|
776
|
+
this.util_validate_data(sys_sch.editable_metadata_schema, meta);
|
|
777
|
+
|
|
689
778
|
// duplicate meta.link check
|
|
690
|
-
if(meta.link){
|
|
691
|
-
let link_search = await this.search({
|
|
779
|
+
if (meta.link) {
|
|
780
|
+
let link_search = await this.search({
|
|
781
|
+
selector: { "meta.link": meta.link },
|
|
782
|
+
});
|
|
692
783
|
console.log(link_search);
|
|
693
784
|
if (link_search.docs.length > 0) {
|
|
694
785
|
throw new Error("This link already exists in the database");
|
|
@@ -697,9 +788,9 @@ export class BeanBagDB {
|
|
|
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.
|
|
793
|
+
this.util_validate_schema_object(data);
|
|
703
794
|
}
|
|
704
795
|
// @TODO : check if single record setting is set to true
|
|
705
796
|
|
|
@@ -735,16 +826,257 @@ export class BeanBagDB {
|
|
|
735
826
|
return doc_obj;
|
|
736
827
|
}
|
|
737
828
|
|
|
738
|
-
|
|
739
|
-
|
|
829
|
+
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
830
|
+
////// Utility methods /////////////////////////////////////////////////////////////////////////////
|
|
831
|
+
///////////////////////////////////////////////////////////////////////////////////////////////////
|
|
832
|
+
|
|
833
|
+
|
|
834
|
+
/**
|
|
835
|
+
* Returns the current Unix timestamp in seconds.
|
|
836
|
+
* divide by 1000 (Date.now gives ms) to convert to seconds. 1 s = 1000 ms
|
|
837
|
+
* @private
|
|
838
|
+
* @returns {number}
|
|
839
|
+
*/
|
|
840
|
+
util_get_now_unix_timestamp() {
|
|
841
|
+
return Math.floor(Date.now() / 1000);
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
/**
|
|
845
|
+
* Validates that the required fields are present in the provided object.
|
|
846
|
+
*
|
|
847
|
+
* @param {string[]} requiredFields - An array of field names that are required.
|
|
848
|
+
* @param {object} obj - The object to check for the required fields.
|
|
849
|
+
* @throws {ValidationError} If any of the required fields are missing, an error is thrown.
|
|
850
|
+
*/
|
|
851
|
+
util_check_required_fields(requiredFields, obj) {
|
|
740
852
|
for (const field of requiredFields) {
|
|
741
|
-
if (!obj[field]) {
|
|
853
|
+
if (!obj[field]) {
|
|
854
|
+
throw new ValidationError([
|
|
855
|
+
{ message: `The field ${field} is required.` },
|
|
856
|
+
]);
|
|
857
|
+
}
|
|
742
858
|
}
|
|
743
859
|
}
|
|
860
|
+
|
|
861
|
+
/**
|
|
862
|
+
* Filters an object, returning a new object that only contains the specified fields.
|
|
863
|
+
*
|
|
864
|
+
* @param {Object} obj - The object to filter.
|
|
865
|
+
* @param {Array<String>} fields - An array of field names to retain in the filtered object.
|
|
866
|
+
*
|
|
867
|
+
* @returns {Object} - A new object containing only the fields that exist in `obj` from the `fields` array.
|
|
868
|
+
*
|
|
869
|
+
* **Example**:
|
|
870
|
+
*
|
|
871
|
+
* const data = { name: "Alice", age: 25, location: "NY" };
|
|
872
|
+
* const result = util_filter_object(data, ["name", "age"]);
|
|
873
|
+
* // result: { name: "Alice", age: 25 }
|
|
874
|
+
*/
|
|
875
|
+
util_filter_object(obj, fields) {
|
|
876
|
+
return fields.reduce((filteredObj, field) => {
|
|
877
|
+
if (Object.prototype.hasOwnProperty.call(obj, field)) {
|
|
878
|
+
filteredObj[field] = obj[field];
|
|
879
|
+
}
|
|
880
|
+
return filteredObj;
|
|
881
|
+
}, {});
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
|
|
885
|
+
/**
|
|
886
|
+
* Validates a data object against a provided JSON schema
|
|
887
|
+
* It relies on the external API provided by the user
|
|
888
|
+
* @param {Object} schema_obj - The JSON schema object to validate against
|
|
889
|
+
* @param {Object} data_obj - The data object to validate
|
|
890
|
+
* @throws {Error} If the data object does not conform to the schema
|
|
891
|
+
*/
|
|
892
|
+
util_validate_data(schema_obj, data_obj) {
|
|
893
|
+
const { valid, validate } = this.utils.validate_schema(
|
|
894
|
+
schema_obj,
|
|
895
|
+
data_obj
|
|
896
|
+
);
|
|
897
|
+
//const ajv = new Ajv({code: {esm: true}}) // options can be passed, e.g. {allErrors: true}
|
|
898
|
+
//const validate = ajv.compile(schema_obj);
|
|
899
|
+
//const valid = validate(data_obj);
|
|
900
|
+
if (!valid) {
|
|
901
|
+
throw new ValidationError(validate.errors);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/**
|
|
906
|
+
* Validates the structure and content of a schema object.
|
|
907
|
+
*
|
|
908
|
+
* This method checks the following conditions:
|
|
909
|
+
*
|
|
910
|
+
* - The schema must have a 'type' field, which should be 'object'.
|
|
911
|
+
* - The 'properties' field must be an object and contain at least one property.
|
|
912
|
+
* - The 'additionalProperties' field must be present and of type boolean.
|
|
913
|
+
* - Primary keys must be defined in the schema and cannot be of type 'object' or 'array'.
|
|
914
|
+
* - Non-editable fields must be defined in the schema.
|
|
915
|
+
* - Encrypted fields must be defined in the schema, of type 'string', and cannot include primary keys.
|
|
916
|
+
*
|
|
917
|
+
* If any of these conditions are violated, an array of error messages will be
|
|
918
|
+
* collected and thrown as a ValidationError.
|
|
919
|
+
*
|
|
920
|
+
* @param {Object} schema_doc - The schema document to validate.
|
|
921
|
+
* @param {Object} schema_doc.schema - The schema structure containing:
|
|
922
|
+
* @param {String} schema_doc.schema.type - The type of the schema (must be 'object').
|
|
923
|
+
* @param {Object} schema_doc.schema.properties - The properties defined in the schema.
|
|
924
|
+
* @param {Boolean} schema_doc.schema.additionalProperties - Indicates if additional properties are allowed.
|
|
925
|
+
* @param {Object} schema_doc.settings - The settings associated with the schema, including:
|
|
926
|
+
* @param {Array<String>} schema_doc.settings.primary_keys - List of primary keys for the schema.
|
|
927
|
+
* @param {Array<String>} schema_doc.settings.non_editable_fields - Fields that cannot be edited.
|
|
928
|
+
* @param {Array<String>} schema_doc.settings.encrypted_fields - Fields that require encryption.
|
|
929
|
+
*
|
|
930
|
+
* @throws {ValidationError} If any validation checks fail, with an array of error messages.
|
|
931
|
+
*/
|
|
932
|
+
util_validate_schema_object(schema_doc) {
|
|
933
|
+
let errors = [{ message: "Schema validation errors " }];
|
|
934
|
+
if (!schema_doc["schema"]["type"]) {
|
|
935
|
+
errors.push({
|
|
936
|
+
message:
|
|
937
|
+
"Schema must have the field schema.'type' which can only be 'object' ",
|
|
938
|
+
});
|
|
939
|
+
} else {
|
|
940
|
+
if (schema_doc["schema"]["type"] != "object") {
|
|
941
|
+
errors.push({
|
|
942
|
+
message: "The schema.'type' value is invalid.Only 'object' allowed",
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
if (!schema_doc["schema"]["properties"]) {
|
|
947
|
+
errors.push({
|
|
948
|
+
message: "The schema.'properties' object does not exists",
|
|
949
|
+
});
|
|
950
|
+
} else {
|
|
951
|
+
if (typeof schema_doc["schema"]["properties"] != "object") {
|
|
952
|
+
errors.push({
|
|
953
|
+
message:
|
|
954
|
+
"Invalid schema.properties. It must be an object and must have atleast one field inside.",
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
if (Object.keys(schema_doc["schema"]["properties"]).length == 0) {
|
|
958
|
+
errors.push({ message: "You must define at least one property" });
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
if (!schema_doc["schema"]["additionalProperties"]) {
|
|
963
|
+
errors.push({
|
|
964
|
+
message: "The schema.'additionalProperties' field is required",
|
|
965
|
+
});
|
|
966
|
+
} else {
|
|
967
|
+
if (typeof schema_doc["schema"]["additionalProperties"] != "boolean") {
|
|
968
|
+
errors.push({
|
|
969
|
+
message:
|
|
970
|
+
"Invalid schema.additionalProperties. It must be a boolean value",
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
const allKeys = Object.keys(schema_doc["schema"]["properties"]);
|
|
976
|
+
if (schema_doc["settings"]["primary_keys"].length > 0) {
|
|
977
|
+
// check if all keys belong to the schema and are not of type object
|
|
978
|
+
let all_pk_exist = schema_doc["settings"]["primary_keys"].every(
|
|
979
|
+
(item) =>
|
|
980
|
+
allKeys.includes(item) &&
|
|
981
|
+
schema_doc["schema"]["properties"][item]["type"] != "object" &&
|
|
982
|
+
schema_doc["schema"]["properties"][item]["type"] != "array"
|
|
983
|
+
);
|
|
984
|
+
|
|
985
|
+
if (!all_pk_exist) {
|
|
986
|
+
errors.push({
|
|
987
|
+
message:
|
|
988
|
+
"Primary keys invalid. All keys must be defined in the schema and must be non object",
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
if (schema_doc["settings"]["non_editable_fields"].length > 0) {
|
|
994
|
+
// check if all keys belong to the schema
|
|
995
|
+
let all_ne_exist = schema_doc["settings"]["non_editable_fields"].every(
|
|
996
|
+
(item) => allKeys.includes(item)
|
|
997
|
+
);
|
|
998
|
+
if (!all_ne_exist) {
|
|
999
|
+
errors.push({
|
|
1000
|
+
message:
|
|
1001
|
+
"Non editable fields invalid. All fields must be defined in the schema ",
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
if (schema_doc["settings"]["encrypted_fields"].length > 0) {
|
|
1007
|
+
// check if all keys belong to the schema and are only string
|
|
1008
|
+
let all_enc_exist = schema_doc["settings"]["encrypted_fields"].every(
|
|
1009
|
+
(item) =>
|
|
1010
|
+
allKeys.includes(item) &&
|
|
1011
|
+
schema_doc["schema"]["properties"][item]["type"] == "string"
|
|
1012
|
+
);
|
|
1013
|
+
if (!all_enc_exist) {
|
|
1014
|
+
errors.push({
|
|
1015
|
+
message:
|
|
1016
|
+
"Invalid encrypted fields. All fields must be defined in the schema and must be string ",
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
// check : primary keys cannot be encrypted
|
|
1021
|
+
let all_enc_no_pk = schema_doc["settings"]["encrypted_fields"].every(
|
|
1022
|
+
(item) => !schema_doc["settings"]["primary_keys"].includes(item)
|
|
1023
|
+
);
|
|
1024
|
+
if (!all_enc_no_pk) {
|
|
1025
|
+
errors.push({
|
|
1026
|
+
message:
|
|
1027
|
+
"Invalid encrypted fields.Primary key fields cannot be encrypted ",
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
/// cannot encrypt primary field keys
|
|
1033
|
+
if (errors.length > 1) {
|
|
1034
|
+
throw new ValidationError(errors);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
/**
|
|
1039
|
+
* Generates a random link composed of four words from a predefined dictionary.
|
|
1040
|
+
*
|
|
1041
|
+
* The words are selected randomly, and the resulting link is formatted as
|
|
1042
|
+
* a hyphen-separated string. This can be useful for creating link for documents.
|
|
1043
|
+
*
|
|
1044
|
+
* @returns {String} A hyphen-separated string containing four randomly
|
|
1045
|
+
* selected words from the dictionary. For example:
|
|
1046
|
+
* "banana-earth-kiwi-rain".
|
|
1047
|
+
*
|
|
1048
|
+
*/
|
|
1049
|
+
util_generate_random_link() {
|
|
1050
|
+
// prettier-ignore
|
|
1051
|
+
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'];
|
|
1052
|
+
return Array.from(
|
|
1053
|
+
{ length: 4 },
|
|
1054
|
+
() => dictionary[Math.floor(Math.random() * dictionary.length)]
|
|
1055
|
+
).join("-");
|
|
1056
|
+
}
|
|
744
1057
|
}
|
|
745
1058
|
|
|
1059
|
+
////////////////// Error classes ////////////////////////////////////////////////////
|
|
746
1060
|
|
|
1061
|
+
/**
|
|
1062
|
+
* This is common for all error classes
|
|
1063
|
+
* @typedef {Object} ErrorItem
|
|
1064
|
+
* @property {string} [instancePath] - The path where the error occurred, optional.
|
|
1065
|
+
* @property {string} message - The error message.
|
|
1066
|
+
*/
|
|
1067
|
+
|
|
1068
|
+
/**
|
|
1069
|
+
* Custom error class for validation errors.
|
|
1070
|
+
*
|
|
1071
|
+
* @extends {Error}
|
|
1072
|
+
*/
|
|
747
1073
|
export class ValidationError extends Error {
|
|
1074
|
+
/**
|
|
1075
|
+
* Custom error class for validation errors.
|
|
1076
|
+
*
|
|
1077
|
+
* @extends {Error}
|
|
1078
|
+
* @param {ErrorItem[]} [errors=[]] - An array of error objects, each containing details about validation failures.
|
|
1079
|
+
*/
|
|
748
1080
|
constructor(errors = []) {
|
|
749
1081
|
// Create a message based on the list of errors
|
|
750
1082
|
//console.log(errors)
|
|
@@ -756,7 +1088,20 @@ export class ValidationError extends Error {
|
|
|
756
1088
|
}
|
|
757
1089
|
}
|
|
758
1090
|
|
|
1091
|
+
|
|
1092
|
+
|
|
1093
|
+
/**
|
|
1094
|
+
* Custom error class for document update errors.
|
|
1095
|
+
*
|
|
1096
|
+
* @extends {Error}
|
|
1097
|
+
*/
|
|
759
1098
|
export class DocUpdateError extends Error {
|
|
1099
|
+
/**
|
|
1100
|
+
* Custom error class for document update errors.
|
|
1101
|
+
*
|
|
1102
|
+
* @extends {Error}
|
|
1103
|
+
* @param {ErrorItem[]} [errors=[]] - An array of error objects, each containing details about validation failures.
|
|
1104
|
+
*/
|
|
760
1105
|
constructor(errors=[]){
|
|
761
1106
|
let error_messages = errors.map(item=>`${item.message}`)
|
|
762
1107
|
let message = `Error in document update. ${error_messages.join(",")}`
|
|
@@ -766,7 +1111,18 @@ export class DocUpdateError extends Error {
|
|
|
766
1111
|
}
|
|
767
1112
|
}
|
|
768
1113
|
|
|
1114
|
+
/**
|
|
1115
|
+
* Custom error class for document insert errors.
|
|
1116
|
+
*
|
|
1117
|
+
* @extends {Error}
|
|
1118
|
+
*/
|
|
769
1119
|
export class DocInsertError extends Error {
|
|
1120
|
+
/**
|
|
1121
|
+
* Custom error class for document insert errors.
|
|
1122
|
+
*
|
|
1123
|
+
* @extends {Error}
|
|
1124
|
+
* @param {ErrorItem[]} [errors=[]] - An array of error objects, each containing details about validation failures.
|
|
1125
|
+
*/
|
|
770
1126
|
constructor(errors=[]){
|
|
771
1127
|
let error_messages = errors.map(item=>`${item.message}`)
|
|
772
1128
|
let message = `Error in document insert. ${error_messages.join(",")}`
|
|
@@ -776,7 +1132,18 @@ export class DocInsertError extends Error {
|
|
|
776
1132
|
}
|
|
777
1133
|
}
|
|
778
1134
|
|
|
1135
|
+
/**
|
|
1136
|
+
* Custom error class for document not found errors.
|
|
1137
|
+
*
|
|
1138
|
+
* @extends {Error}
|
|
1139
|
+
*/
|
|
779
1140
|
export class DocNotFoundError extends Error {
|
|
1141
|
+
/**
|
|
1142
|
+
* Custom error class for document not found errors.
|
|
1143
|
+
*
|
|
1144
|
+
* @extends {Error}
|
|
1145
|
+
* @param {ErrorItem[]} [errors=[]] - An array of error objects, each containing details about validation failures.
|
|
1146
|
+
*/
|
|
780
1147
|
constructor(errors=[]){
|
|
781
1148
|
let error_messages = errors.map(item=>`${item.message}`)
|
|
782
1149
|
let message = `Error in fetching document. Criteria : ${error_messages.join(",")}`
|