beanbagdb 0.5.53 → 0.5.60

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.
@@ -1 +1,64 @@
1
- # Getting started
1
+ # Getting started
2
+
3
+
4
+
5
+ ### Nodes and edges
6
+
7
+ The records stored in BeanBagDB can be organized into a simple directed graph. In in simple directed graph, edges have a direction and there can only be one edge between 2 nodes. Edges can have label based on the context of your data. You can also define rules for these edges based on the schema of the nodes.
8
+
9
+ Example : Consider you have 3 types of nodes : "player", "team", "match" to simulate a game where a teams has multiple players and a player plays a match as part of a team.
10
+ Here are the rules:
11
+ - A player is part of a team (and not vice versa)
12
+ - A team plays a match (and not vice versa)
13
+ - A player cannot be in 2 teams
14
+ - A match can only be between 2 teams (not more than 2)
15
+ - Player does not play a match (but a team does, assuming this is a team sport)
16
+ - A team cannot be part of a team
17
+ - A team cannot have more than 11 players
18
+
19
+ In terms of nodes and edges, the rules translate to:
20
+
21
+ | rule | node 1 | edge label | node 2 | constraint |
22
+ |-------------------------------------------------------|--------|-------------|--------|---------------------------------|
23
+ | 1 A player is part of a team (and not vice versa) | player | is part of | team | player-->team |
24
+ | 2 A team plays a match (and not vice versa) | team | plays | match | |
25
+ | 3 A player cannot be in 2 teams | player | is part of | team | only one such relation |
26
+ | 4 A match can only be between 2 teams (not more than 2) | team | plays | match | only 2 such relation per match |
27
+ | 5 Player does not play a match | player | plays | match | not valid |
28
+ | 6 A team cannot be part of a team | team | is part of | team | not valid |
29
+ | 7 A team cannot have more than 11 players | player | is part of | team | only 11 such relations per team |
30
+
31
+
32
+ These rules can be enforced in the database as "edge_constraints" and are automatically checked when new edges are added to the graph.
33
+
34
+ ```
35
+ - Constraint 1 :
36
+ - node 1 : player
37
+ - edge type : part_of
38
+ - edge label : is part of a
39
+ - node 2 : team
40
+ - max_from_node1 : 1
41
+ - max_to_node2 : 11
42
+ - Constraint 2 :
43
+ - node 1 : team
44
+ - edge type : plays
45
+ - edge label : plays
46
+ - node 2 : match
47
+ - max_from_node1 : None
48
+ - max_to_node2 : 2
49
+ ```
50
+ - `max_from_node1` defines the maximum number of edges of a certain type that can exist from node 1 to node 2. For example, player1 is part of team1; now, player1 cannot be part of any other team.
51
+ - `max_to_node2` defines the maximum number of edges of a certain type that can be directed towards a particular node. For example, team1 and team2 can only play match1.
52
+ - Violations of these results in an error and the edge is not created
53
+ - for both node1 and node2, the general approach is to whitelist a set of schemas for the specified type of edge. It is still valid to add other types of edges from these nodes to others.
54
+ - The schema names in node1 and node2 fixes the schema types for this particular edge type. This means creating a relations "player1--(part_of)-->team1" or "team1--(part_of)-->player1" will result in the same relation "player1--(part_of)-->team1".
55
+ - You can also specify nodes with different schema types as : "player,coach".
56
+ - If you want to allow nodes of all schema types, use "*."
57
+ - Eg :
58
+ - Constraint:
59
+ - node 1: *
60
+ - edge label: interacts with
61
+ - node 2: *
62
+ - If you want to include all except for a few types of nodes, use : "* - schema1, schema2"
63
+ - Eg : "*-tag tagged_as tag" means that nodes of all types (except for tag nodes themselves) can be tagged with a tag.
64
+
@@ -1 +1,23 @@
1
- # How to use plugins
1
+ # How to use plugins
2
+
3
+ ```
4
+ // MyPlugin.js
5
+ const MyPlugin = {
6
+ // This function will be bound to the class instance (passed as 'thisArg')
7
+ on_load: async function(instance) {
8
+ console.log("Plugin loaded with instance:", instance);
9
+ // You can access instance properties and methods here
10
+ instance.someMethod();
11
+ },
12
+
13
+ // Another plugin function
14
+ doSomething: function(instance, arg) {
15
+ console.log("Plugin doing something with arg:", arg);
16
+ // Access the class instance here as 'instance'
17
+ instance.someMethod();
18
+ }
19
+ };
20
+
21
+ export default MyPlugin;
22
+
23
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "beanbagdb",
3
- "version": "0.5.53",
3
+ "version": "0.5.60",
4
4
  "description": "A JS library to introduce a schema layer to a No-SQL local database",
5
5
  "main": "src/index.js",
6
6
  "module": "src/index.js",
package/src/index.js CHANGED
@@ -344,6 +344,7 @@ export class BeanBagDB {
344
344
  async create(schema, data, meta = {}, settings = {}) {
345
345
  this._check_ready_to_use();
346
346
  if(!schema){throw new DocCreationError(`No schema provided`)}
347
+ if(schema=="setting_edge"){throw new DocCreationError("This type of record can only be created through the create_edge api")}
347
348
  if(Object.keys(data).length==0){throw new DocCreationError(`No data provided`)}
348
349
  try {
349
350
  let doc_obj = await this._insert_pre_checks(schema, data,meta, settings);
@@ -440,7 +441,6 @@ export class BeanBagDB {
440
441
  * - Retrieves the document based on the provided search criteria.
441
442
  * - Checks the revision ID to detect potential conflicts. (To be implemented: behavior when the `rev_id` does not match).
442
443
  * - 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
444
  * - Encrypts fields if encryption is required by the schema settings.
445
445
  * - Updates the `meta` fields (such as `updated_on` and `updated_by`) and saves the updated document to the database.
446
446
  *
@@ -448,7 +448,7 @@ export class BeanBagDB {
448
448
  * @returns {Object} The result of the document update operation.
449
449
  *
450
450
  * **Errors**:
451
- * - Throws an error if a document with the same primary keys already exists (and `single_record == false`).
451
+ * - Throws an error if a document with the same primary keys already exists .
452
452
  * - Throws a `DocUpdateError` if a primary key conflict is detected during the update.
453
453
  *
454
454
  * @throws {DocUpdateError} - If a document with conflicting primary keys already exists.
@@ -548,7 +548,7 @@ export class BeanBagDB {
548
548
  async delete(criteria) {
549
549
  this._check_ready_to_use();
550
550
  let doc = await this.read(criteria)
551
- const delete_blocked = ["schema","setting",""]
551
+ const delete_blocked = ["schema","setting","key"]
552
552
  if (delete_blocked.includes(doc.schema)){
553
553
  throw new Error(`Deletion of ${doc.schema} doc is not support yet.`)
554
554
  }
@@ -597,6 +597,7 @@ export class BeanBagDB {
597
597
  async get(special_doc_type,criteria={}){
598
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
599
  const fetch_docs = {
600
+ // to return schema object for the given name
600
601
  schema:async (criteria)=>{
601
602
  let schemaSearch = await this.db_api.search({
602
603
  selector: { schema: "schema", "data.name": criteria.name },
@@ -606,6 +607,30 @@ export class BeanBagDB {
606
607
  throw new DocNotFoundError(BeanBagDB.error_codes.schema_not_found);
607
608
  }
608
609
  return schemaSearch.docs[0];
610
+ },
611
+ // schema list
612
+ schema_list:async (criteria)=>{
613
+ let schemaSearch = await this.db_api.search({
614
+ selector: { schema: "schema" },
615
+ });
616
+ // console.log(schemaSearch)
617
+ if (schemaSearch.docs.length == 0) {
618
+ throw new DocNotFoundError(BeanBagDB.error_codes.schema_not_found);
619
+ }else{
620
+ let schemas = []
621
+ schemaSearch.docs.map(doc=>{
622
+ schemas.push({
623
+ name: doc.data.name,
624
+ version: doc.data.version,
625
+ system_defined : doc.data.system_generated,
626
+ description: doc.data.description,
627
+ link: doc.meta.link,
628
+ _id:doc._id
629
+ })
630
+ })
631
+ return schemas
632
+ }
633
+
609
634
  }
610
635
  }
611
636
  if(Object.keys(fetch_docs).includes(special_doc_type)){
@@ -630,6 +655,119 @@ export class BeanBagDB {
630
655
  }
631
656
  }
632
657
 
658
+ ///////////////////////////////////////////////////////////
659
+ //////////////// simple directed graph ////////////////////////
660
+ //////////////////////////////////////////////////////////
661
+
662
+ async create_edge(node1,node2,edge_name,edge_label=""){
663
+ this._check_ready_to_use();
664
+ if(!edge_name){throw new ValidationError("edge_name required")}
665
+ if(Object.keys(node1)==0){throw new ValidationError("node1 required")}
666
+ if(Object.keys(node2)==0){throw new ValidationError("node2 required")}
667
+
668
+ let n1 = await this.read(node1)
669
+ let n2 = await this.read(node2)
670
+ let edges_constraint
671
+
672
+ try {
673
+ let d = await this.read({schema:"setting_edge_constraint",data:{name:edge_name}})
674
+ edges_constraint = d["doc"]["data"]
675
+ let errors = []
676
+ let node1id = n1.doc._id
677
+ let node2id = n2.doc._id
678
+ let val_check = this._check_nodes_edge(edges_constraint.node1,edges_constraint.node2,n1.doc.schema,n2.doc.schema)
679
+
680
+ if (val_check.valid){
681
+ if(val_check.swapped){
682
+ // swapping required
683
+ node1id = n2.doc._id
684
+ node2id = n1.doc._id
685
+ }
686
+ }else{
687
+ this.errors.push("Invalid nodes.This config of nodes not allowed")
688
+ }
689
+
690
+ let records = await this.search({selector:{schema:"system_edge","data.edge_name":edge_name}})
691
+
692
+ if(edges_constraint.max_from_node1!=-1){
693
+ let filter1 = records.docs.filter((itm)=>itm.data.node1==node1id)
694
+ if(filter1.length>=edges_constraint.max_from_node1){
695
+ errors.push("max limit reached")
696
+ }
697
+ }
698
+
699
+ if(edges_constraint.max_to_node2!=-1){
700
+ let filter2 = records.docs.filter((itm)=>itm.data.node2==node2id)
701
+ if(filter1.length>=edges_constraint.max_from_node1){
702
+ errors.push("max limit reached")
703
+ }
704
+ }
705
+
706
+ if(errors.length==0){
707
+ let edge = await this.create("system_edge",{node1: node1id , node2: node1id ,edge_name:edge_name })
708
+ return edge
709
+ }else{
710
+ throw new RelationError(errors)
711
+ }
712
+
713
+ } catch (error) {
714
+ if(error instanceof DocNotFoundError){
715
+ let doc = {node1:"*",node2:"*",name:edge_name,label:edge_label}
716
+ let new_doc = await this.create("system_edge_constraint",doc)
717
+ let edge = await this.create("system_edge",{node1: n1.doc._id,node2: n2.doc._id,edge_name:edge_name })
718
+ return edge
719
+ }else{
720
+ throw error
721
+ }
722
+ }
723
+ }
724
+
725
+ _check_node_schema_match(rule, schema) {
726
+ /**
727
+ * Check if the schema matches the rule. The rule can be:
728
+ * - "*" for any schema
729
+ * - "*-n1,n2" for all schemas except n1 and n2
730
+ * - "specific_schema" or "schema1,schema2" for specific schema matches
731
+ */
732
+ if (rule === "*") {
733
+ return true;
734
+ }
735
+
736
+ if (rule.startsWith("*-")) {
737
+ // Exclude the schemas listed after "*-"
738
+ const exclusions = rule.slice(2).split(",");
739
+ return !exclusions.includes(schema);
740
+ }
741
+
742
+ // Otherwise, check if schema matches the specific rule (comma-separated for multiple allowed schemas)
743
+ const allowedSchemas = rule.split(",");
744
+ return allowedSchemas.includes(schema);
745
+ }
746
+
747
+ _check_nodes_edge(node1Rule, node2Rule, schema1, schema2) {
748
+ /**
749
+ * Check if the edge between schema1 (node1) and schema2 (node2) is valid based on the rules
750
+ * node1Rule and node2Rule. Also checks if the nodes should be swapped.
751
+ *
752
+ */
753
+ // Check if schema1 matches node1Rule and if schema2 matches node2Rule
754
+ const matchesNode1 = this._check_node_schema_match(node1Rule, schema1);
755
+ const matchesNode2 = this._check_node_schema_match(node2Rule, schema2);
756
+
757
+ // Check if schema1 matches node2Rule and schema2 matches node1Rule (for swapping condition)
758
+ const matchesSwappedNode1 = this._check_node_schema_match(node2Rule, schema1);
759
+ const matchesSwappedNode2 = this._check_node_schema_match(node1Rule, schema2);
760
+
761
+ // If the schemas match their respective rules (node1 and node2), the edge is valid
762
+ if (matchesNode1 && matchesNode2) { return { valid: true, swapped: false }}
763
+
764
+ // If swapping makes it valid, indicate that the nodes should be swapped
765
+ if (matchesSwappedNode1 && matchesSwappedNode2) { return { valid: true, swapped: true }}
766
+ // Otherwise, the edge is invalid
767
+ return { valid: false, swapped: false };
768
+ }
769
+
770
+
633
771
  ///////////////////////////////////////////////////////////
634
772
  //////////////// Internal methods ////////////////////////
635
773
  //////////////////////////////////////////////////////////
@@ -1145,4 +1283,29 @@ constructor(errors=[]){
1145
1283
  this.name = "EncryptionError";
1146
1284
  this.errors = errors
1147
1285
  }
1286
+ }
1287
+
1288
+ /**
1289
+ * Custom error class for relation error.
1290
+ *
1291
+ * @extends {Error}
1292
+ */
1293
+ export class RelationError extends Error {
1294
+ /**
1295
+ *
1296
+ * @extends {Error}
1297
+ * @param {ErrorItem[]} [errors=[]] - An array of error objects, each containing details about validation failures.
1298
+ */
1299
+ constructor(errors=[]){
1300
+ let error_messages
1301
+ if(Array.isArray(errors)){
1302
+ error_messages = errors.map(item=>` ${(item.instancePath||" ").replace("/","")} ${item.message} `)
1303
+ }else {
1304
+ error_messages = [errors]
1305
+ }
1306
+ let message = `Error in relation of the simple digraph : ${error_messages.join(",")}`
1307
+ super(message)
1308
+ this.name = "RelationError";
1309
+ this.errors = errors
1310
+ }
1148
1311
  }
@@ -0,0 +1,193 @@
1
+
2
+
3
+
4
+ const commands = {
5
+ new: {
6
+ parse: async (instance,parts) => {
7
+ let criteria = {}
8
+ criteria.schema = parts.length==0?"":parts.join("")
9
+ return criteria
10
+ },
11
+ run: async (instance,command) => {
12
+ if (command.criteria.schema==""){
13
+ // return a list of all schemas present in the DB
14
+ let all_schema = await instance.get("schema_list")
15
+ return all_schema
16
+ }else{
17
+ // return the schema object for the given schema if not found throw error
18
+ let schema_obj = await instance.search({"selector":{"schema":"schema","data.name":command.criteria.schema}})
19
+ //console.log(schema_obj)
20
+ if(schema_obj.docs.length==0){
21
+ throw new Error("Schema with this name does not exists")
22
+ }
23
+ return schema_obj.docs[0]
24
+ }
25
+ },
26
+ help: `To create a new record. Format : new/"schema_name(optional)". If no schema name provided, a list of valid schema name is returned`
27
+ },
28
+ open:{
29
+ parse: async (instance,parts) => {
30
+ let criteria = {}
31
+ if (parts.length==0){
32
+ throw new Error("Invalid arguments.open command needs unique id")
33
+ }
34
+ let id_type = parts[0]
35
+ if(id_type=="id"){
36
+ parts.shift()
37
+ criteria["_id"] = parts.join("")
38
+ }else if(id_type=="link"){
39
+ parts.shift()
40
+ criteria["link"] = parts.join("")
41
+ }else if(id_type=="key"){
42
+ parts.shift()
43
+ let text= parts.join()
44
+ let p = text.split(",")
45
+
46
+ p.map(itm=>{
47
+ let p1 = itm.split("=")
48
+ if(p1[0]=="schema"){
49
+ criteria["schema"] = p1[1]
50
+ }else{
51
+ criteria["data"][p1[0]] = p1[1]
52
+ }
53
+ })
54
+
55
+ if(!criteria["schema"]){
56
+ throw new Error("Key requires a schema")
57
+ }
58
+ }else{
59
+ throw new Error("Invalid unique key")
60
+ }
61
+ return criteria
62
+ },
63
+ run: async (instance,command) => {
64
+ try {
65
+ let data = await instance.read(command.criteria,true)
66
+ return data
67
+ } catch (error) {
68
+ throw error
69
+ }
70
+ },
71
+ help: `To open a record using it's unique id. Format : open/"id||link|key"/"value". In case of key field name must be provided as : field1=value1,fields2=value2...`
72
+ },
73
+ tool:{
74
+ parse : async (instance,parts)=>{
75
+ let criteria = {}
76
+ criteria.type = parts.length==0?"info":parts.join("")
77
+ return criteria
78
+ },
79
+ run : async (instance,command)=>{
80
+ let c_type = command.criteria.type
81
+ let data = {}
82
+ if (c_type=="info"){
83
+ // to get all basic info about the database
84
+ let data = {
85
+ meta: instance.metadata(),
86
+ schemas : {},
87
+ logs:[]
88
+ }
89
+ let schemas = await instance.get("schema_list")
90
+ data.schemas = schemas
91
+
92
+ let logs_doc = await instance.read({"schema":"system_settings","data":{name:"system_logs"}})
93
+ //console.log(logs_doc)
94
+ data =logs_doc.doc.data.value
95
+ return data
96
+
97
+ }else if(c_type=="plugins"){
98
+ // to show list of all plugins installed
99
+ // todo later not implemented yet
100
+ }else if(c_type=="settings"){
101
+ // to show the list of all setting docs available
102
+ let search = instance.search({"selector":{"schema":"system_settings"}})
103
+ return {docs:search.docs}
104
+ }
105
+ else if(c_type=="keys"){
106
+ // to show the list of all keys present in the db
107
+ let search = instance.search({"selector":{"schema":"system_keys"}})
108
+ return {docs:search.docs}
109
+ }
110
+ else{
111
+ throw new Error("Invalid tool command")
112
+ }
113
+ },
114
+ }
115
+ };
116
+
117
+ const parse = async (instance, text) => {
118
+ let data = {
119
+ errors: [],
120
+ valid: false,
121
+ name: "",
122
+ criteria: {},
123
+ };
124
+ if (!text) {
125
+ data.errors.push(
126
+ "No text command provided. Format : command_name/parameter"
127
+ );
128
+ }
129
+ let parts = text.split("/");
130
+ if (parts.length == 0) {
131
+ data.errors.push("Invalid text command");
132
+ }
133
+ let command_name = parts[0];
134
+ if (!commands[command_name]) {
135
+ data.errors.push(
136
+ "Invalid command name. Valid : " + Object.keys(commands).join(",")
137
+ );
138
+ }
139
+ data.name = command_name;
140
+ try {
141
+ parts.shift();
142
+ let criteria = await commands[command_name].parse(instance,parts);
143
+ data.criteria = criteria;
144
+ } catch (error) {
145
+ data.errors.push(error.message);
146
+ }
147
+ if (data.errors.length == 0) {
148
+ data.valid = true;
149
+ }
150
+ return data;
151
+ };
152
+
153
+ const run = async (instance, command) => {
154
+ let data = {
155
+ result:{},
156
+ errors:[],
157
+ valid:false
158
+ };
159
+
160
+ if (!command) {
161
+ data.errors.push("No command object provided ");
162
+ }
163
+ if (!command.valid){
164
+ data.errors["Command cannot be run"]
165
+
166
+ }
167
+ if(!commands[command.name]){
168
+ data.errors["Invalid command name"]
169
+ }
170
+
171
+ try {
172
+ let data1 = await commands[command.name].run(instance,command)
173
+ //console.log(data)
174
+ data.result = data1
175
+ } catch (error) {
176
+ data.errors.push(error.message)
177
+ }
178
+ if(data.errors.length==0){
179
+ data.valid = true
180
+ }
181
+ return data
182
+ };
183
+
184
+ const parse_and_run = async(instance, text) => {
185
+ let command = await parse(instance,text)
186
+ console.log(command)
187
+ let command_result = await run(instance,command)
188
+ return command_result
189
+ }
190
+
191
+ export const text_command = {
192
+ parse,run,parse_and_run
193
+ };