Skip to content

Commit

Permalink
Merge pull request #29 from shubhvjain/main
Browse files Browse the repository at this point in the history
rel schema and some fixes
  • Loading branch information
shubhvjain authored Nov 2, 2024
2 parents 40a386b + 8f4d994 commit e5a336f
Show file tree
Hide file tree
Showing 7 changed files with 636 additions and 56 deletions.
65 changes: 64 additions & 1 deletion doc-src/1_getting-started.md
Original file line number Diff line number Diff line change
@@ -1 +1,64 @@
# Getting started
# Getting started



### Nodes and edges

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.

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.
Here are the rules:
- A player is part of a team (and not vice versa)
- A team plays a match (and not vice versa)
- A player cannot be in 2 teams
- A match can only be between 2 teams (not more than 2)
- Player does not play a match (but a team does, assuming this is a team sport)
- A team cannot be part of a team
- A team cannot have more than 11 players

In terms of nodes and edges, the rules translate to:

| rule | node 1 | edge label | node 2 | constraint |
|-------------------------------------------------------|--------|-------------|--------|---------------------------------|
| 1 A player is part of a team (and not vice versa) | player | is part of | team | player-->team |
| 2 A team plays a match (and not vice versa) | team | plays | match | |
| 3 A player cannot be in 2 teams | player | is part of | team | only one such relation |
| 4 A match can only be between 2 teams (not more than 2) | team | plays | match | only 2 such relation per match |
| 5 Player does not play a match | player | plays | match | not valid |
| 6 A team cannot be part of a team | team | is part of | team | not valid |
| 7 A team cannot have more than 11 players | player | is part of | team | only 11 such relations per team |


These rules can be enforced in the database as "edge_constraints" and are automatically checked when new edges are added to the graph.

```
- Constraint 1 :
- node 1 : player
- edge type : part_of
- edge label : is part of a
- node 2 : team
- max_from_node1 : 1
- max_to_node2 : 11
- Constraint 2 :
- node 1 : team
- edge type : plays
- edge label : plays
- node 2 : match
- max_from_node1 : None
- max_to_node2 : 2
```
- `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.
- `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.
- Violations of these results in an error and the edge is not created
- 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.
- 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".
- You can also specify nodes with different schema types as : "player,coach".
- If you want to allow nodes of all schema types, use "*."
- Eg :
- Constraint:
- node 1: *
- edge label: interacts with
- node 2: *
- If you want to include all except for a few types of nodes, use : "* - schema1, schema2"
- Eg : "*-tag tagged_as tag" means that nodes of all types (except for tag nodes themselves) can be tagged with a tag.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "beanbagdb",
"version": "0.5.54",
"version": "0.5.60",
"description": "A JS library to introduce a schema layer to a No-SQL local database",
"main": "src/index.js",
"module": "src/index.js",
Expand Down
142 changes: 140 additions & 2 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,7 @@ export class BeanBagDB {
async create(schema, data, meta = {}, settings = {}) {
this._check_ready_to_use();
if(!schema){throw new DocCreationError(`No schema provided`)}
if(schema=="setting_edge"){throw new DocCreationError("This type of record can only be created through the create_edge api")}
if(Object.keys(data).length==0){throw new DocCreationError(`No data provided`)}
try {
let doc_obj = await this._insert_pre_checks(schema, data,meta, settings);
Expand Down Expand Up @@ -440,15 +441,14 @@ export class BeanBagDB {
* - Retrieves the document based on the provided search criteria.
* - Checks the revision ID to detect potential conflicts. (To be implemented: behavior when the `rev_id` does not match).
* - Validates editable fields against `schema.settings.editable_fields` (or allows editing of all fields if not specified).
* - Performs primary key conflict checks if multiple records are allowed (`single_record == false`).
* - Encrypts fields if encryption is required by the schema settings.
* - Updates the `meta` fields (such as `updated_on` and `updated_by`) and saves the updated document to the database.
*
* **Returns**:
* @returns {Object} The result of the document update operation.
*
* **Errors**:
* - Throws an error if a document with the same primary keys already exists (and `single_record == false`).
* - Throws an error if a document with the same primary keys already exists .
* - Throws a `DocUpdateError` if a primary key conflict is detected during the update.
*
* @throws {DocUpdateError} - If a document with conflicting primary keys already exists.
Expand Down Expand Up @@ -655,6 +655,119 @@ export class BeanBagDB {
}
}

///////////////////////////////////////////////////////////
//////////////// simple directed graph ////////////////////////
//////////////////////////////////////////////////////////

async create_edge(node1,node2,edge_name,edge_label=""){
this._check_ready_to_use();
if(!edge_name){throw new ValidationError("edge_name required")}
if(Object.keys(node1)==0){throw new ValidationError("node1 required")}
if(Object.keys(node2)==0){throw new ValidationError("node2 required")}

let n1 = await this.read(node1)
let n2 = await this.read(node2)
let edges_constraint

try {
let d = await this.read({schema:"setting_edge_constraint",data:{name:edge_name}})
edges_constraint = d["doc"]["data"]
let errors = []
let node1id = n1.doc._id
let node2id = n2.doc._id
let val_check = this._check_nodes_edge(edges_constraint.node1,edges_constraint.node2,n1.doc.schema,n2.doc.schema)

if (val_check.valid){
if(val_check.swapped){
// swapping required
node1id = n2.doc._id
node2id = n1.doc._id
}
}else{
this.errors.push("Invalid nodes.This config of nodes not allowed")
}

let records = await this.search({selector:{schema:"system_edge","data.edge_name":edge_name}})

if(edges_constraint.max_from_node1!=-1){
let filter1 = records.docs.filter((itm)=>itm.data.node1==node1id)
if(filter1.length>=edges_constraint.max_from_node1){
errors.push("max limit reached")
}
}

if(edges_constraint.max_to_node2!=-1){
let filter2 = records.docs.filter((itm)=>itm.data.node2==node2id)
if(filter1.length>=edges_constraint.max_from_node1){
errors.push("max limit reached")
}
}

if(errors.length==0){
let edge = await this.create("system_edge",{node1: node1id , node2: node1id ,edge_name:edge_name })
return edge
}else{
throw new RelationError(errors)
}

} catch (error) {
if(error instanceof DocNotFoundError){
let doc = {node1:"*",node2:"*",name:edge_name,label:edge_label}
let new_doc = await this.create("system_edge_constraint",doc)
let edge = await this.create("system_edge",{node1: n1.doc._id,node2: n2.doc._id,edge_name:edge_name })
return edge
}else{
throw error
}
}
}

_check_node_schema_match(rule, schema) {
/**
* Check if the schema matches the rule. The rule can be:
* - "*" for any schema
* - "*-n1,n2" for all schemas except n1 and n2
* - "specific_schema" or "schema1,schema2" for specific schema matches
*/
if (rule === "*") {
return true;
}

if (rule.startsWith("*-")) {
// Exclude the schemas listed after "*-"
const exclusions = rule.slice(2).split(",");
return !exclusions.includes(schema);
}

// Otherwise, check if schema matches the specific rule (comma-separated for multiple allowed schemas)
const allowedSchemas = rule.split(",");
return allowedSchemas.includes(schema);
}

_check_nodes_edge(node1Rule, node2Rule, schema1, schema2) {
/**
* Check if the edge between schema1 (node1) and schema2 (node2) is valid based on the rules
* node1Rule and node2Rule. Also checks if the nodes should be swapped.
*
*/
// Check if schema1 matches node1Rule and if schema2 matches node2Rule
const matchesNode1 = this._check_node_schema_match(node1Rule, schema1);
const matchesNode2 = this._check_node_schema_match(node2Rule, schema2);

// Check if schema1 matches node2Rule and schema2 matches node1Rule (for swapping condition)
const matchesSwappedNode1 = this._check_node_schema_match(node2Rule, schema1);
const matchesSwappedNode2 = this._check_node_schema_match(node1Rule, schema2);

// If the schemas match their respective rules (node1 and node2), the edge is valid
if (matchesNode1 && matchesNode2) { return { valid: true, swapped: false }}

// If swapping makes it valid, indicate that the nodes should be swapped
if (matchesSwappedNode1 && matchesSwappedNode2) { return { valid: true, swapped: true }}
// Otherwise, the edge is invalid
return { valid: false, swapped: false };
}


///////////////////////////////////////////////////////////
//////////////// Internal methods ////////////////////////
//////////////////////////////////////////////////////////
Expand Down Expand Up @@ -1170,4 +1283,29 @@ constructor(errors=[]){
this.name = "EncryptionError";
this.errors = errors
}
}

/**
* Custom error class for relation error.
*
* @extends {Error}
*/
export class RelationError extends Error {
/**
*
* @extends {Error}
* @param {ErrorItem[]} [errors=[]] - An array of error objects, each containing details about validation failures.
*/
constructor(errors=[]){
let error_messages
if(Array.isArray(errors)){
error_messages = errors.map(item=>` ${(item.instancePath||" ").replace("/","")} ${item.message} `)
}else {
error_messages = [errors]
}
let message = `Error in relation of the simple digraph : ${error_messages.join(",")}`
super(message)
this.name = "RelationError";
this.errors = errors
}
}
Loading

0 comments on commit e5a336f

Please sign in to comment.