├── package.json ├── LICENSE ├── index.benchmark.js ├── .gitignore ├── joqular.js ├── examples └── basic.js ├── index.test.js ├── src ├── operators.js └── operators.test.js ├── index.js └── README.md /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lmdb-oql", 3 | "version": "0.5.8", 4 | "description": "A high level object query language for indexed LMDB databases", 5 | "type": "module", 6 | "main": "index.js", 7 | "scripts": { 8 | "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/anywhichway/lmdb-oql.git" 13 | }, 14 | "keywords": [ 15 | "lmdb", 16 | "no-sql", 17 | "select", 18 | "index", 19 | "firebase", 20 | "mongo" 21 | ], 22 | "author": "Simon Y. Blackwell", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/anywhichway/lmdb-oql/issues" 26 | }, 27 | "homepage": "https://github.com/anywhichway/lmdb-oql#readme", 28 | "devDependencies": { 29 | "benchmark": "^2.1.4", 30 | "jest": "^29.5.0", 31 | "lmdb": "^2.7.11" 32 | }, 33 | "dependencies": { 34 | "@anywhichway/cartesian-product": "^1.0.5", 35 | "lmdb-index": "^0.11.3" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Simon Y. Blackwell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /index.benchmark.js: -------------------------------------------------------------------------------- 1 | import {open} from "lmdb"; 2 | import {withExtensions} from "./index.js"; 3 | 4 | const benchmark = await import("./node_modules/benchmark/benchmark.js"), 5 | Benchmark = benchmark.default, 6 | suite = new Benchmark.Suite; 7 | 8 | const db = withExtensions(open("test.db",{useVersions:true})); 9 | db.clearSync(); 10 | db.defineSchema(Object); 11 | //for await (const item of db.insert().into(Object).values({Object:{name:"joe",age:21,random:1,address:{city:"New York",state:"NY"}}})) {}; 12 | await db.insert().into(Object).values({Object:{name:"joe",age:21,random:1,address:{city:"New York",state:"NY"}}}).exec(); 13 | const select = db.select().from(Object).where({Object:{random:1}}); 14 | const insert = db.insert().into(Object).values({Object:{name:"joe",age:21,address:{city:"New York",state:"NY"},random:Math.random()}}); 15 | await db.put(1,1); 16 | suite.add("put primitive",async () => { 17 | const key = await db.put(1,1); 18 | if(key!==1) console.log(new Error("Key is not 1")); 19 | }) 20 | suite.add("get primitive",() => { 21 | const value = db.get(1); 22 | if(value!==1) console.log(new Error("Value is not 1")); 23 | }) 24 | suite.add("get primitive from disk",async () => { 25 | const v0 = db.get(1); 26 | const v1 = db.cache.get(1); 27 | db.cache.delete(1); 28 | const v2 = db.get(1); 29 | //if(v2!==1) console.log(new Error("Value is not 1")); 30 | }) 31 | suite.add("getEntry primitive",() => { 32 | const {value} = db.getEntry(1); 33 | if(value!==1) console.log(new Error("Value is not 1")); 34 | }) 35 | suite.add("select.exec",() => { 36 | let count = 0; 37 | for(const item of select.exec()) { 38 | count++ 39 | } 40 | if(count===0) console.log(new Error("select.exec no items found")); 41 | }) 42 | suite.add("select",() => { 43 | let count = 0; 44 | for(const item of db.select().from(Object).where({Object:{random:1}})) { 45 | count++ 46 | } 47 | if(count===0) console.log(new Error("select no items found")); 48 | }) 49 | suite.add("put indexed object",async () => { 50 | await db.put(null,{name:"joe",age:21,address:{city:"New York",state:"NY"},random:Math.random()}); 51 | }) 52 | suite.add("insert with change",async () => { 53 | await db.insert().into(Object).values({Object:{name:"joe",age:21,address:{city:"New York",state:"NY"},random:Math.random()}}); 54 | }) 55 | suite.add("insert.exec",async () => { 56 | await insert.exec(); 57 | }) 58 | 59 | .on('cycle', function(event) { 60 | console.log(String(event.target)); 61 | }) 62 | .on('complete', function() { 63 | console.log('Fastest is ' + this.filter('fastest').map('name')); 64 | }) 65 | .run({ maxTime:5 }); -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | -------------------------------------------------------------------------------- /joqular.js: -------------------------------------------------------------------------------- 1 | import {operators as OPERATORS} from "./src/operators.js"; 2 | 3 | class FilterArray extends Array { } 4 | 5 | function functionalize(key,value,filters=new FilterArray()) { 6 | if(arguments.length===1) return functionalize(null,key); 7 | if(OPERATORS[key] || (typeof(value)==="function" && value.name[0]==="$")) { 8 | const f = OPERATORS[key] || value, 9 | test = typeof(value)==="function" ? undefined : value; 10 | filters.push((right,options={}) => f(right, Object.assign({...options},test ? {test} : undefined))); 11 | } 12 | if(value && typeof(value)=="object" && !(value instanceof FilterArray)) { 13 | const result = {}; 14 | return Object.entries(value).reduce((result,[k,v]) => { 15 | if(OPERATORS[k] || (typeof(v)==="function" && v.name[0]==="$")) { 16 | result instanceof FilterArray || (result = Object.assign(new FilterArray(),result)); 17 | functionalize(k,v,result); 18 | } else { 19 | result[k] = functionalize(k,v); 20 | } 21 | return result; 22 | },{}) 23 | } 24 | return value; 25 | } 26 | const select = (pattern,{all,isFunctionalized}={}) => { 27 | pattern = isFunctionalized ? pattern : functionalize(pattern); 28 | if(!isFunctionalized) console.log(pattern) 29 | return { 30 | from(where, {result = {}, root = result, parent, key} = {}) { 31 | const type = typeof (pattern); 32 | if (type === "function") { 33 | return pattern(where, {root, parent, key}); 34 | } 35 | if (pattern && type === "object") { 36 | if (pattern instanceof RegExp) { 37 | if (typeof (where) === "string") { 38 | const match = where.match(pattern); 39 | return match ? match[0] : undefined; 40 | } 41 | return; 42 | } 43 | if (pattern instanceof FilterArray) { 44 | let final; 45 | pattern.every((f) => { 46 | final = f(where, {root,parent,key}); 47 | return final !== undefined; 48 | }) 49 | return final; 50 | } 51 | if(all) { 52 | Object.entries(where).forEach(([key,value]) => { 53 | result[key] = value && typeof(value)==="object" ? {...value} : value 54 | }) 55 | } 56 | Object.entries(pattern).forEach(([key, value]) => { 57 | // todo add RegExp matching for keys 58 | value = select(value, {all,isFunctionalized:true}).from(where[key], {root, parent:result, key}); 59 | if(value === undefined) { 60 | delete result[key]; 61 | } else { 62 | result[key] = value; 63 | } 64 | }) 65 | if(Object.keys(result).length>0) return result; 66 | } 67 | if (pattern === where) { 68 | return where; 69 | } 70 | } 71 | } 72 | } 73 | 74 | console.log(select({name:{$eq: "joe",$test(value) { return value.toUpperCase(); }},address:{city:{$eq:"Seattle",$lift(value,{root,key}) { root[key] = value; }}}},{all:false}).from({age:21,name:"joe",address:{city:"Seattle",zip:"98101"}})); 75 | const str = JSON.stringify({name:{$eq: "joe",$test(value) { return value.toUpperCase(); }},address:{city:{$eq:"Seattle",$lift(value,{root,key}) { root[key] = value; }}}}); 76 | console.log(str); 77 | const parsed = JSON.parse(str,functionalize); 78 | console.log(parsed); 79 | console.log(select(parsed,{isFunctionalized:true}).from({age:21,name:"joe",address:{city:"Seattle",zip:"98101"}})) 80 | -------------------------------------------------------------------------------- /examples/basic.js: -------------------------------------------------------------------------------- 1 | import {open} from "lmdb"; 2 | import {operators,withExtensions,IDS} from "../index.js"; 3 | 4 | const {$mod,$gte} = operators; 5 | class Person { 6 | constructor(config={}) { 7 | Object.assign(this,config); 8 | } 9 | } 10 | class Employer { 11 | constructor(config={}) { 12 | Object.assign(this,config); 13 | } 14 | } 15 | 16 | const db = withExtensions(open("test")); 17 | db.clearSync(); 18 | db.defineSchema(Person); 19 | db.defineSchema(Employer); 20 | 21 | // typically you do no provide an id for a put of a an instance controlled by a schema 22 | const personId = await db.put(null,new Person({name:"bill",age:21,employer:"ACME"})); 23 | // but you can if you want to, so long as you at start it with the class name followed by @ 24 | await db.put("Employer@1",new Employer({name:"ACME",address:"123 Main St."})); 25 | 26 | const person = await db.get(personId); 27 | // if a return value is an object that is controlled by a schema, 28 | // it will be an instance of the schema's class 29 | console.log(person); 30 | /* 31 | Person { 32 | name: 'bill', 33 | age: 21, 34 | employer: 'ACME', 35 | '#': 'Person@850ad934-a449-493e-846a-96e00a1b6546' 36 | } 37 | */ 38 | 39 | // you can use predefined operators in place of literal matches 40 | console.log([...db.select().from(Person).where({Person:{age:$gte(21)}})]); 41 | /* 42 | [ 43 | { 44 | Person: Person { 45 | name: 'bill', 46 | age: 21, 47 | employer: 'ACME', 48 | '#': 'Person@850ad934-a449-493e-846a-96e00a1b6546' 49 | } 50 | } 51 | ] 52 | */ 53 | // there are lots of operators, Person has an odd numbered age, could use $odd 54 | console.log([...db.select().from(Person).where({Person:{age:$mod([2,1])}})]); 55 | /* 56 | [ 57 | { 58 | Person: Person { 59 | name: 'bill', 60 | age: 21, 61 | employer: 'ACME', 62 | '#': 'Person@850ad934-a449-493e-846a-96e00a1b6546' 63 | } 64 | } 65 | ] 66 | */ 67 | 68 | // joins are performed using the class name as the key 69 | // this example joins Person to Employer on Person.employer === Employer.name 70 | console.log([...db.select().from(Person,Employer).where({Person:{employer: {Employer:{name:"ACME"}}}})]); 71 | /* 72 | [ 73 | { 74 | Person: Person { 75 | name: 'bill', 76 | age: 21, 77 | employer: 'ACME', 78 | '#': 'Person@850ad934-a449-493e-846a-96e00a1b6546' 79 | }, 80 | Employer: Employer { 81 | name: 'ACME', 82 | address: '123 Main St.', 83 | '#': 'Employer@c5e07e94-4a94-4cfc-b167-65cb1dd7bd29' 84 | } 85 | } 86 | ] 87 | */ 88 | 89 | // class aliases are supported by providing two element arrays in 'from' 90 | // with the first element being the class and the second being the alias 91 | console.log([...db.select().from([Person, "P"],[Employer,"E"]).where({P:{employer: {E:{name:"ACME"}}}})]); 92 | /* 93 | [ 94 | { 95 | Person: Person { 96 | name: 'bill', 97 | age: 21, 98 | employer: 'ACME', 99 | '#': 'Person@850ad934-a449-493e-846a-96e00a1b6546' 100 | }, 101 | Employer: Employer { 102 | name: 'ACME', 103 | address: '123 Main St.', 104 | '#': 'Employer@c5e07e94-4a94-4cfc-b167-65cb1dd7bd29' 105 | } 106 | } 107 | ] 108 | */ 109 | 110 | // you can select just the data you want and move it up a level 111 | console.log([...db.select({P:{name(value,{root}) { root.name=value; }},E:{address(value,{root}){ root.workAddress=value; }}}) 112 | .from([Person, "P"],[Employer,"E"]) 113 | .where({P:{employer: {E:{name:"ACME"}}}})]); 114 | /* 115 | [ { name: 'bill', workAddress: '123 Main St.' } ] 116 | */ 117 | 118 | // you can select just ids 119 | console.log([...db.select(IDS).from([Person, "P"],[Employer,"E"]).where({P:{employer: {E:{name:"ACME"}}}})]) 120 | /* 121 | [ [ 'Person@64fc6554-066c-47dd-a99e-d0492dcb957c', 'Employer@1' ] ] 122 | */ 123 | -------------------------------------------------------------------------------- /index.test.js: -------------------------------------------------------------------------------- 1 | import {open} from "lmdb"; 2 | import {withExtensions,operators,IDS} from "./index.js"; 3 | 4 | const {$and,$or,$not,$eq,$gte,$isCreditCard,$isEmail,$isNull,$isSSN,$isURL,$type} = operators; 5 | 6 | class Person { 7 | constructor(props) { 8 | Object.assign(this,props); 9 | } 10 | } 11 | 12 | const parent = withExtensions(open("test.db",{useVersions:true})), 13 | child = withExtensions(parent.openDB("child",{useVersions:true})), 14 | dbs = [parent,child]; 15 | for (let i=0;i { 29 | for await(const id of db.insert().into(Person).values({Person:{name:"joe",age:21,notes:null,address:{city:"New York",state:"NY"}}})) { 30 | const person = db.get(id); 31 | expect(person.name).toBe("joe"); 32 | expect(person).toBeInstanceOf(Person); 33 | } 34 | }) 35 | test(`insert with exec ${i}`,async () => { 36 | for (const id of await db.insert().into(Person).values({Person:{name:"joe",age:21,notes:null,address:{city:"New York",state:"NY"}}}).exec()) { 37 | const person = db.get(id); 38 | expect(person.name).toBe("joe"); 39 | expect(person).toBeInstanceOf(Person); 40 | await db.remove(id); 41 | } 42 | }) 43 | 44 | test(`insert array ${i}`,async () => { 45 | for await(const id of db.insert().into(Array).values({Array:[[1,2,3]]})) { 46 | const array = db.get(id); 47 | expect(array.length).toBe(3); 48 | expect(array["#"]).toBe(id); 49 | expect(array).toBeInstanceOf(Array); 50 | delete array["#"]; 51 | expect(array).toEqual([1,2,3]); 52 | } 53 | }) 54 | 55 | test(`insert array throws ${i}`,async () => { 56 | try { 57 | for await(const id of db.insert().into(Array).values({Array:[1,2,3]})) { 58 | throw new Error("should have thrown") 59 | } 60 | } catch(e) { 61 | expect(e.message).toBe("Expected array of arrays when inserting Array"); 62 | return; 63 | }; 64 | throw new Error("should have thrown") 65 | }) 66 | 67 | test(`simple select ${i}`,() => { 68 | const results = [...db.select().from(Person).where({Person:{name:"joe",age:$gte(21),notes:$isNull(),pronoun:$type("string"),website:$isURL(),CC:$isCreditCard(),email:$isEmail()}})]; 69 | expect(results.length).toBe(1) 70 | }) 71 | 72 | test(`simple select $and ${i}`,() => { 73 | const results = [...db.select().from(Person).where({Person:{name:"joe",age:$and($gte(21),$gte(21))}})]; 74 | expect(results.length).toBe(2) 75 | }) 76 | 77 | test(`simple select $or ${i}`,() => { 78 | const results = [...db.select().from(Person).where({Person:{name:"joe",age:$or($eq(20),$gte(21))}})]; 79 | expect(results.length).toBe(2) 80 | }) 81 | 82 | test(`simple select $not ${i}`,() => { 83 | const results = [...db.select().from(Person).where({Person:{name:"joe",age:$not($eq(20))}})]; 84 | expect(results.length).toBe(2) 85 | }) 86 | 87 | test(`select IDS ${i}`,() => { 88 | const results = [...db.select(IDS).from(Person).where({Person:{name:"joe",age:$gte(21),notes:$isNull(),pronoun:$type("string"),website:$isURL(),CC:$isCreditCard(),email:$isEmail()}})]; 89 | expect(results.length).toBe(1) 90 | }) 91 | 92 | test(`select with literal ${i}`,async () => { 93 | const results = [...db.select().from([Person,"P1"],[Person,"P2"]).where({P1: {name: {P2: {name: (value)=>value}}, age:21}})]; 94 | expect(results.length).toBe(4) 95 | }) 96 | test(`select with function ${i}`,async () => { 97 | const results = [...db.select().from([Person,"P1"],[Person,"P2"]).where({P1: {age:(value)=>value===21 ? value : undefined, name: {P2: {name: (value)=>value}}}})]; 98 | expect(results.length).toBe(4) 99 | }) 100 | test(`select with right operator ${i}`,async () => { 101 | const results = [...db.select().from([Person,"P1"],[Person,"P2"]).where({P1: {age:(value)=>value===21 ? value : undefined, name: {P2: {name: $eq()}}}})]; 102 | expect(results.length).toBe(4) 103 | }) 104 | test(`select right outer join with right operator ${i}`,async () => { 105 | const results = [...db.select().from([Person,"P1"],[Person,"P2"]).where({P1: {age:(value)=>value===21 ? value : undefined, name: {P2: {name: $eq("joe")}}}})]; 106 | expect(results.length).toBe(4) 107 | }) 108 | test(`select none with function ${i}`,async () => { 109 | const results = [...db.select().from([Person,"P1"],[Person,"P2"]).where({P1: {age:(value)=>value===22 ? value : undefined, name: {P2: {name: (value)=>value}}}})]; 110 | expect(results.length).toBe(0) 111 | }) 112 | test(`select join ${i}`,async () => { 113 | const results = [...db.select().from([Person,"P1"],[Person,"P2"]).where({P1: {name: {P2: {name: (left,right)=>left===right}}}})]; 114 | expect(results.length).toBe(4) 115 | }) 116 | test(`select with selector ${i}`,async () => { 117 | const results = [...db.select({P1:{name:(value)=>value}}).from([Person,"P1"],[Person,"P2"]).where({P1: {name: {P2: {name: (left,right)=>left===right}}}})]; 118 | expect(results.length).toBe(4); 119 | expect(results[0]).toEqual({ P1: { name: 'joe' } } ) 120 | }) 121 | test(`patch ${i}`,async () => { 122 | for await(const id of db.update(Person).set({Person:{age:22}}).where({Person:{name:"joe"}})) { 123 | const person = db.get(id); 124 | expect(person.age).toBe(22); 125 | } 126 | expect([...db.select().from(Person).where({Person:{age:22}})].length).toBe(2); 127 | }) 128 | test(`delete ${i}`,async () => { 129 | for await(const id of db.delete().from(Person).where({Person:{name:"joe"}})) { 130 | expect(db.get(id)).toBe(undefined); 131 | }; 132 | expect([...db.select().from(Person).where({Person:{name:"joe"}})].length).toBe(0); 133 | }) 134 | test(`delete with exec ${i}`,async () => { 135 | const results = await db.delete().from().where().exec(); 136 | expect(results.length).toBe(0); 137 | }) 138 | test(`update with exec ${i}`,async () => { 139 | const results = await db.update().set().where().exec(); 140 | expect(results.length).toBe(0); 141 | }) 142 | test(`select with exec ${i}`,async () => { 143 | const results = db.select().from().where().exec(); 144 | expect(results.length).toBe(0); 145 | }) 146 | } 147 | -------------------------------------------------------------------------------- /src/operators.js: -------------------------------------------------------------------------------- 1 | //soundex from https://gist.github.com/shawndumas/1262659 2 | function soundex(a) {a=(a+"").toLowerCase().split("");var c=a.shift(),b="",d={a:"",e:"",i:"",o:"",u:"",b:1,f:1,p:1,v:1,c:2,g:2,j:2,k:2,q:2,s:2,x:2,z:2,d:3,t:3,l:4,m:5,n:5,r:6},b=c+a.map(function(a){return d[a]}).filter(function(a,b,e){return 0===b?a!==d[c]:a!==e[b-1]}).join("");return(b+"000").slice(0,4).toUpperCase()}; 3 | 4 | const validateLuhn = num => { 5 | let arr = (num + '') 6 | .split('') 7 | .reverse() 8 | .map(x => parseInt(x)); 9 | let lastDigit = arr.splice(0, 1)[0]; 10 | let sum = arr.reduce((acc, val, i) => (i % 2 !== 0 ? acc + val : acc + ((val * 2) % 9) || 9), 0); 11 | sum += lastDigit; 12 | return sum % 10 === 0; 13 | } 14 | 15 | const operators = { 16 | 17 | //$and 18 | //$or 19 | //$not 20 | //$xor 21 | //$ior 22 | 23 | 24 | $type(right, {test}) { 25 | return typeof(right)===test ? right : undefined 26 | }, 27 | $isOdd(value) { 28 | return value%2===1 ? value : undefined 29 | }, 30 | $isEven(value) { 31 | return value%2===0 ? value : undefined 32 | }, 33 | $isPositive(value) { 34 | return value>0 ? value : undefined 35 | }, 36 | $isNegative(value) { 37 | return value<0 ? value : undefined 38 | }, 39 | $isInteger(value) { 40 | return Number.isInteger(value) ? value : undefined 41 | }, 42 | $isFloat(value) { 43 | const str = value+"", 44 | parts = str.split("."); 45 | return parts.length==2 ? value : undefined 46 | }, 47 | $isNaN(value) { 48 | return Number.isNaN(value) ? value : undefined 49 | }, 50 | $isTruthy(value) { 51 | return value ? value : undefined 52 | }, 53 | $isFalsy(value) { 54 | return !value ? value : undefined 55 | }, 56 | $isNull(value) { 57 | return value===null ? value : undefined 58 | }, 59 | $isUndefined(value) { 60 | return value===undefined ? value : undefined 61 | }, 62 | $isDefined(value) { 63 | return value!==undefined ? value : undefined 64 | }, 65 | $isPrimitive(value) { 66 | const type = typeof(value); 67 | return !["object","function"].includes(type) ? value : undefined; 68 | }, 69 | $isArray(value) { 70 | return Array.isArray(value) ? value : undefined 71 | }, 72 | $isCreditCard(value) { 73 | // Visa || Mastercard || American Express || Diners Club || Discover || JCB 74 | return typeof(value)==="string" && (/(?:\d[ -]*?){13,16}/g).test(value) && validateLuhn(value) ? value : undefined; 75 | }, 76 | $isEmail(value) { 77 | return typeof(value)==="string" && (!/(\.{2}|-{2}|_{2})/.test(value) && /^[a-z0-9][a-z0-9-_\.]+@[a-z0-9][a-z0-9-]+[a-z0-9]\.[a-z]{2,10}(?:\.[a-z]{2,10})?$/i).test(value) ? value : undefined; 78 | }, 79 | $isURL(value) { 80 | return typeof(value)==="string" && (/^(?:https?|ftp):\/\/[^\s/$.?#].[^\s]*$/is).test(value) ? value : undefined; 81 | }, 82 | $isUUID(value) { 83 | return typeof(value)==="string" && (/^[a-f\d]{8}(-[a-f\d]{4}){4}[a-f\d]{8}$/is).test(value) ? value : undefined; 84 | }, 85 | $isIPAddress(value) { 86 | return typeof(value)==="string" && (/(([2]([0-4][0-9]|[5][0-5])|[0-1]?[0-9]?[0-9])[.]){3}(([2]([0-4][0-9]|[5][0-5])|[0-1]?[0-9]?[0-9]))/gi).test(value) ? value : undefined; 87 | }, 88 | $isSSN(value) { 89 | return typeof(value)==="string" && (/^\d{3}-?\d{2}-?\d{4}$/is).test(value) ? value : undefined; 90 | }, 91 | $isISBN(value) { 92 | return typeof(value)==="string" && (/^(?:ISBN(?:-1[03])?:?\s)?(?=[-0-9\s]{17}$|[-0-9X\s]{13}$|[0-9X]{10}$)(?:97[89][-\s]?)?[0-9]{1,5}[-\s]?(?:[0-9]+[-\s]?){2}[0-9X]$/).test(value) ? value : undefined; 93 | }, 94 | $isZIPCode(value) { 95 | return typeof(value)==="string" && (/[0-9]{5}(-[0-9]{4})?/g).test(value) ? value : undefined; 96 | }, 97 | 98 | $lt(right, {test}) { 99 | return right=test ? right : undefined 115 | }, 116 | $gt(right, {test}) { 117 | return right>test ? right : undefined 118 | }, 119 | 120 | $between(right, {test}) { 121 | return right>=test[0] && right<=test[1] ? right : undefined 122 | }, 123 | $outside(right, {test}) { 124 | return righttest[1] ? right : undefined 125 | }, 126 | $in(right, {test}) { 127 | return test.includes(right) ? right : undefined 128 | }, 129 | $nin(right, {test}) { 130 | return !test.includes(right) ? right : undefined 131 | }, 132 | $includes(right, {test}) { 133 | return test.includes && test.includes(right) ? right : undefined 134 | }, 135 | $excludes(right, {test}) { 136 | return !test.includes || !test.includes(right) ? right : undefined 137 | }, 138 | 139 | $intersects(right, {test}) { 140 | return Array.isArray(right) && Array.isArray(test) && right.some((item) => test.includes(item)) ? right : undefined 141 | }, 142 | $disjoint(right, {test}) { 143 | return Array.isArray(right) && Array.isArray(test) && !right.some((item) => test.includes(item)) ? right : undefined 144 | }, 145 | $subset(right, {test}) { 146 | return Array.isArray(right) && Array.isArray(test) && right.every((item) => test.includes(item)) ? right : undefined 147 | }, 148 | $superset(right, {test}) { 149 | return Array.isArray(right) && Array.isArray(test) && test.every((item) => right.includes(item)) ? right : undefined 150 | }, 151 | $symmetric(right, {test}) { 152 | return Array.isArray(right) && Array.isArray(test) && right.length===test.length && right.every((item) => test.includes(item)) ? right : undefined 153 | }, 154 | $startsWith(right, {test}) { 155 | test = typeof(test)==="number" ? test+"" : test; 156 | return right.startsWith && right.startsWith(test) ? right : undefined 157 | }, 158 | $endsWith(right, {test}) { 159 | test = typeof(test)==="number" ? test+"" : test; 160 | return right.endsWith && right.endsWith(test) ? right : undefined 161 | }, 162 | $length(right, {test}) { 163 | return right.length==test ? right : undefined 164 | }, 165 | 166 | $matches(right, {test}) { 167 | const value = typeof(right)==="number" ? right+"" : right; 168 | return typeof(value)==="string" && value.match(test) ? right : undefined 169 | }, 170 | $echoes(right, {test}) { 171 | right = typeof(right)==="number" ? right+"" : right; 172 | test = typeof(test)==="number" ? test+"" : test; 173 | return typeof(right)==="string" && typeof(test)==="string" && soundex(right)===soundex(test) ? right : undefined 174 | }, 175 | 176 | 177 | $add(right, {test}) { 178 | return right+test[0]===test[1] ? right : undefined 179 | }, 180 | $subtract(right, {test}) { 181 | return right-test[0]===test[1] ? right : undefined 182 | }, 183 | $multiply(right, {test}) { 184 | return right*test[0]===test[1] ? right : undefined 185 | }, 186 | $divide(right, {test}) { 187 | return right/test[0]===test[1] ? right : undefined 188 | }, 189 | $mod(right, {test}) { 190 | return right%test[0]===test[1] ? right : undefined 191 | }, 192 | $pow(right, {test}) { 193 | return right**test[0]===test[1] ? right : undefined 194 | }, 195 | 196 | /* 197 | $$if(right, {test}) { 198 | return right===test[0] ? test[1] : test[2] 199 | }, 200 | $$case(right, {test}) { 201 | const dflt = test.length/2!==0 ? test.pop() : undefined, 202 | pair = () => test.length>0 ? [test.shift(), test.shift()] : undefined; 203 | let next; 204 | while(next=pair()) { 205 | if(next[0]===right) return next[1]; 206 | } 207 | }, 208 | $$concat(right, {test}) { 209 | return Array.isArray(test) && Array.isArray(right) ? right.concat(test) : right + test; 210 | }, 211 | $$join(right, {test}) { 212 | right = Array.isArray(right) ? right : [right]; 213 | return right.join(test) 214 | }, 215 | $$slice(right, {test}) { 216 | return Array.isArray(test) && Array.isArray(right) ? right.slice(...test) : typeof(right)==="string" ? right.substring(...test) : undefined; 217 | }, 218 | $$substring(right, {test}) { 219 | return typeof(right)==="string" ? right.substring(...test) : undefined; 220 | }, 221 | $$replace(right, {test}) { 222 | return typeof(right)==="string" ? right.replace(...test) : undefined; 223 | }, 224 | $$split(right, {test}) { 225 | 226 | }, 227 | $$trim(right, {test}) { 228 | 229 | }, 230 | $$padStart(right, {test}) { 231 | 232 | }, 233 | $$add(right, {test}) { 234 | return typeof(right)==="number" ? right+test : undefined; 235 | }, 236 | $$subtract(right, {test}) { 237 | return typeof(right)==="number" ? right-test : undefined; 238 | }, 239 | $$multiply(right, {test}) { 240 | return typeof(right)==="number" ? right*test : undefined; 241 | }, 242 | $$divide(right, {test}) { 243 | return typeof(right)==="number" ? right/test : undefined; 244 | }, 245 | $$mod(right, {test}) { 246 | return typeof(right)==="number" ? right%test : undefined 247 | }, 248 | $$pow(right, {test}) { 249 | return typeof(right)==="number" ? right**test : undefined; 250 | }, 251 | ...["abs", "ceil", "floor", "round", "sign", "sqrt", "trunc","cos","sin","tan","acos","asin","atan","atan2","exp","log","max","min","random"].reduce((acc,fn) => { 252 | acc["$$"+fn] = (right, {test}) => typeof(right)==="number" ? Math[fn](right) : undefined; 253 | return acc; 254 | },{}) 255 | */ 256 | } 257 | 258 | export {operators as default,operators} -------------------------------------------------------------------------------- /src/operators.test.js: -------------------------------------------------------------------------------- 1 | import {operators} from './operators.js'; 2 | 3 | test("$type",() => { 4 | expect(operators.$type("hello",{test:"string"})).toBe("hello"); 5 | expect(operators.$type("hello",{test:"number"})).toBeUndefined(); 6 | expect(operators.$type(123,{test:"number"})).toBe(123); 7 | expect(operators.$type(123,{test:"string"})).toBeUndefined(); 8 | }) 9 | 10 | test("$lt",() => { 11 | expect(operators.$lt(1, {test: 2})).toBe(1); 12 | expect(operators.$lt(2, {test: 2})).toBeUndefined(); 13 | }) 14 | 15 | test("$lte",() => { 16 | expect(operators.$lte(1, {test: 2})).toBe(1); 17 | expect(operators.$lte(2, {test: 2})).toBe(2); 18 | expect(operators.$lte(3, {test: 2})).toBeUndefined(); 19 | }) 20 | 21 | test("$eq",() => { 22 | expect(operators.$eq(1, {test: 2})).toBeUndefined(); 23 | expect(operators.$eq(2, {test: 2})).toBe(2); 24 | }) 25 | 26 | test("$eeq",() => { 27 | expect(operators.$eeq(1, {test: 2})).toBeUndefined(); 28 | expect(operators.$eeq(2, {test: 2})).toBe(2); 29 | }) 30 | 31 | test("$neq",() => { 32 | expect(operators.$neq(1, {test: 2})).toBe(1); 33 | expect(operators.$neq(2, {test: 2})).toBeUndefined(); 34 | }) 35 | 36 | test("$gte",() => { 37 | expect(operators.$gte(1, {test: 2})).toBeUndefined(); 38 | expect(operators.$gte(2, {test: 2})).toBe(2); 39 | expect(operators.$gte(3, {test: 2})).toBe(3); 40 | }) 41 | 42 | test("$gt",() => { 43 | expect(operators.$gt(1, {test: 2})).toBeUndefined(); 44 | expect(operators.$gt(2, {test: 2})).toBeUndefined(); 45 | expect(operators.$gt(3, {test: 2})).toBe(3); 46 | }) 47 | 48 | test("$between",() => { 49 | expect(operators.$between(1, {test: [2,3]})).toBeUndefined(); 50 | expect(operators.$between(2, {test: [2,3]})).toBe(2); 51 | expect(operators.$between(3, {test: [2,3]})).toBe(3); 52 | expect(operators.$between(4, {test: [2,3]})).toBeUndefined(); 53 | }) 54 | 55 | test("$outside",() => { 56 | expect(operators.$outside(1, {test: [2,3]})).toBe(1); 57 | expect(operators.$outside(2, {test: [2,3]})).toBeUndefined(); 58 | expect(operators.$outside(3, {test: [2,3]})).toBeUndefined(); 59 | expect(operators.$outside(4, {test: [2,3]})).toBe(4); 60 | }) 61 | 62 | test("$in",() => { 63 | expect(operators.$in(1, {test: [2,3]})).toBeUndefined(); 64 | expect(operators.$in(2, {test: [2,3]})).toBe(2); 65 | expect(operators.$in(3, {test: [2,3]})).toBe(3); 66 | expect(operators.$in(4, {test: [2,3]})).toBeUndefined(); 67 | }) 68 | 69 | test("$nin",() => { 70 | expect(operators.$nin(1, {test: [2,3]})).toBe(1); 71 | expect(operators.$nin(2, {test: [2,3]})).toBeUndefined(); 72 | expect(operators.$nin(3, {test: [2,3]})).toBeUndefined(); 73 | expect(operators.$nin(4, {test: [2,3]})).toBe(4); 74 | }) 75 | 76 | test("$includes",() => { 77 | expect(operators.$includes(1, {test: [2, 3]})).toBeUndefined(); 78 | expect(operators.$includes(2, {test: [2, 3]})).toBe(2); 79 | expect(operators.$includes(3, {test: [2, 3]})).toBe(3); 80 | }) 81 | 82 | test("$excludes",() => { 83 | expect(operators.$excludes(1, {test: [2, 3]})).toBe(1); 84 | expect(operators.$excludes(2, {test: [2, 3]})).toBeUndefined(); 85 | expect(operators.$excludes(3, {test: [2, 3]})).toBeUndefined(); 86 | }) 87 | 88 | test("$intersects",() => { 89 | expect(operators.$intersects([1], {test: [2, 3]})).toBeUndefined(); 90 | expect(operators.$intersects([2], {test: [2, 3]})).toEqual([2]); 91 | }) 92 | 93 | test("$disjoint",() => { 94 | expect(operators.$disjoint([1], {test: [2, 3]})).toEqual([1]); 95 | expect(operators.$disjoint([2], {test: [2, 3]})).toBeUndefined(); 96 | }) 97 | 98 | test("$subset",() => { 99 | expect(operators.$subset([1], {test: [2, 3]})).toBeUndefined(); 100 | expect(operators.$subset([2], {test: [2, 3]})).toEqual([2]); 101 | }) 102 | 103 | test("$superset",() => { 104 | expect(operators.$superset([1,2,3], {test: [2, 3]})).toEqual([1,2,3]); 105 | expect(operators.$superset([2], {test: [2, 3]})).toBeUndefined(); 106 | }) 107 | 108 | test("$symmetric",() => { 109 | expect(operators.$symmetric([2,3], {test: [2, 3]})).toEqual([2,3]); 110 | expect(operators.$symmetric([3,2], {test: [2, 3]})).toEqual([3,2]); 111 | expect(operators.$symmetric([2], {test: [2, 3]})).toBeUndefined(); 112 | }) 113 | 114 | test("$startsWith",() => { 115 | expect(operators.$startsWith("1", {test: "2"})).toBeUndefined(); 116 | expect(operators.$startsWith("2", {test: "2"})).toBe("2"); 117 | expect(operators.$startsWith("2", {test: 2})).toBe("2"); 118 | }) 119 | 120 | test("$endsWith",() => { 121 | expect(operators.$endsWith("1", {test: "2"})).toBeUndefined(); 122 | expect(operators.$endsWith("2", {test: "2"})).toBe("2"); 123 | expect(operators.$endsWith("2", {test: 2})).toBe("2"); 124 | }) 125 | 126 | test("$length",() => { 127 | expect(operators.$length("123", {test: 2})).toBeUndefined(); 128 | expect(operators.$length("123", {test: 3})).toBe("123"); 129 | expect(operators.$length([1,2,3], {test: 2})).toBeUndefined(); 130 | expect(operators.$length([1,2,3], {test: 3})).toEqual([1,2,3]); 131 | }) 132 | 133 | test("$matches",() => { 134 | expect(operators.$matches(1, {test: /2/})).toBeUndefined(); 135 | expect(operators.$matches(2, {test: /2/})).toBe(2); 136 | }) 137 | 138 | test("$echoes",() => { 139 | expect(operators.$echoes("lyme", {test: "lime"})).toBe("lyme"); 140 | expect(operators.$echoes("lemon", {test: "apple"})).toBeUndefined(); 141 | }) 142 | 143 | test("$isOdd",() => { 144 | expect(operators.$isOdd(1, {test: true})).toBe(1); 145 | expect(operators.$isOdd(2, {test: true})).toBeUndefined(); 146 | }) 147 | 148 | test("$isEven",() => { 149 | expect(operators.$isEven(1, {test: true})).toBeUndefined(); 150 | expect(operators.$isEven(2, {test: true})).toBe(2); 151 | }) 152 | 153 | test("$isPositive",() => { 154 | expect(operators.$isPositive(1, {test: true})).toBe(1); 155 | expect(operators.$isPositive(-1, {test: true})).toBeUndefined(); 156 | }) 157 | 158 | test("$isNegative",() => { 159 | expect(operators.$isNegative(1, {test: true})).toBeUndefined(); 160 | expect(operators.$isNegative(-1, {test: true})).toBe(-1); 161 | }) 162 | 163 | test("$isInteger",() => { 164 | expect(operators.$isInteger(1, {test: true})).toBe(1); 165 | expect(operators.$isInteger(1.1, {test: true})).toBeUndefined(); 166 | }) 167 | 168 | test("$isFloat",() => { 169 | expect(operators.$isFloat(1, {test: true})).toBeUndefined(); 170 | expect(operators.$isFloat(1.1, {test: true})).toBe(1.1); 171 | }) 172 | 173 | test("$isNaN",() => { 174 | expect(operators.$isNaN(1, {test: true})).toBeUndefined(); 175 | expect(operators.$isNaN(NaN, {test: true})).toBe(NaN); 176 | }) 177 | 178 | test("$isTruthy",() => { 179 | expect(operators.$isTruthy(1, {test: true})).toBe(1); 180 | expect(operators.$isTruthy(0, {test: true})).toBeUndefined(); 181 | }) 182 | 183 | test("$isFalsy",() => { 184 | expect(operators.$isFalsy(1, {test: true})).toBeUndefined(); 185 | expect(operators.$isFalsy(0, {test: true})).toBe(0); 186 | }) 187 | 188 | test("$isNull",() => { 189 | expect(operators.$isNull(1, {test: true})).toBeUndefined(); 190 | expect(operators.$isNull(null, {test: true})).toBe(null); 191 | }) 192 | 193 | test("$isUndefined",() => { 194 | expect(operators.$isUndefined(1, {test: true})).toBeUndefined(); 195 | expect(operators.$isUndefined(undefined, {test: true})).toBe(undefined); 196 | }) 197 | 198 | test("$isDefined",() => { 199 | expect(operators.$isDefined(1, {test: true})).toBe(1); 200 | expect(operators.$isDefined(undefined, {test: true})).toBeUndefined(); 201 | }) 202 | 203 | test("$isPrimitive",() => { 204 | expect(operators.$isPrimitive(1, {test: true})).toBe(1); 205 | expect(operators.$isPrimitive("a", {test: true})).toBe("a"); 206 | expect(operators.$isPrimitive(true, {test: true})).toBe(true); 207 | expect(operators.$isPrimitive(null, {test: true})).toBeUndefined(); 208 | expect(operators.$isPrimitive({a: 1}, {test: true})).toBeUndefined(); 209 | }) 210 | 211 | test("$isArray",() => { 212 | expect(operators.$isArray([1,2,3], {test:true})).toEqual([1,2,3]); 213 | expect(operators.$isArray([1,2,"3"], {test:true})).toEqual([1,2,"3"]); 214 | expect(operators.$isArray(1, {test:true})).toBeUndefined(); 215 | }) 216 | 217 | test("$isCreditCard",() => { 218 | expect(operators.$isCreditCard(1, {test: true})).toBeUndefined(); 219 | expect(operators.$isCreditCard("4012888888881881", {test: true})).toBe("4012888888881881"); 220 | }) 221 | 222 | test("$isEmail",() => { 223 | expect(operators.$isEmail(1, {test: true})).toBeUndefined(); 224 | expect(operators.$isEmail("nobody@nowhere.com", {test: true})).toBe("nobody@nowhere.com"); 225 | }) 226 | 227 | test("$isURL",() => { 228 | expect(operators.$isURL(1, {test: true})).toBeUndefined(); 229 | expect(operators.$isURL("http://www.nowhere.com", {test: true})).toBe("http://www.nowhere.com"); 230 | }) 231 | 232 | test("$isUUID",() => { 233 | expect(operators.$isUUID(1, {test: true})).toBeUndefined(); 234 | expect(operators.$isUUID("550e8400-e29b-41d4-a716-446655440000", {test: true})).toBe("550e8400-e29b-41d4-a716-446655440000"); 235 | }) 236 | 237 | test("$isIPAddress",() => { 238 | expect(operators.$isIPAddress(1, {test: true})).toBeUndefined(); 239 | expect(operators.$isIPAddress("127.0.0.1", {test: true})).toBe("127.0.0.1") 240 | }) 241 | 242 | test("$isSSN",() => { 243 | expect(operators.$isSSN(1, {test: true})).toBeUndefined(); 244 | expect(operators.$isSSN("123-45-6789", {test: true})).toBe("123-45-6789"); 245 | }) 246 | 247 | test("$isISBN",() => { 248 | expect(operators.$isISBN(1, {test: true})).toBeUndefined(); 249 | expect(operators.$isISBN("978-0-596-52068-7", {test: true})).toBe("978-0-596-52068-7"); 250 | }) 251 | 252 | test("$isZIPCode",() => { 253 | expect(operators.$isZIPCode(1, {test: true})).toBeUndefined(); 254 | expect(operators.$isZIPCode("12345", {test: true})).toBe("12345"); 255 | }) 256 | 257 | 258 | 259 | test("$add",() => { 260 | expect(operators.$add(1, {test: [1, 2]})).toBe(1); 261 | expect(operators.$add(1, {test: [1, 3]})).toBeUndefined(); 262 | }) 263 | 264 | test("$subtract",() => { 265 | expect(operators.$subtract(1, {test: [1, 0]})).toBe(1); 266 | expect(operators.$subtract(1, {test: [1, 2]})).toBeUndefined(); 267 | }) 268 | 269 | test("$multiply",() => { 270 | expect(operators.$multiply(1, {test: [1, 1]})).toBe(1); 271 | expect(operators.$multiply(1, {test: [1, 2]})).toBeUndefined(); 272 | }) 273 | 274 | test("$divide",() => { 275 | expect(operators.$divide(1, {test: [1, 1]})).toBe(1); 276 | expect(operators.$divide(1, {test: [1, 2]})).toBeUndefined(); 277 | }) 278 | 279 | test("$mod",() => { 280 | expect(operators.$mod(1, {test: [1, 0]})).toBe(1); 281 | expect(operators.$mod(1, {test: [1, 2]})).toBeUndefined(); 282 | }) 283 | 284 | test("$pow",() => { 285 | expect(operators.$pow(1, {test: [1, 1]})).toBe(1); 286 | expect(operators.$pow(1, {test: [1, 2]})).toBeUndefined(); 287 | }) 288 | 289 | /* 290 | test("$$if",() => { 291 | expect(operators.$$if("hello",{test:["hello","world","goodbye"]})).toBe("world"); 292 | expect(operators.$$if("goodbye",{test:["hello","world","goodbye"]})).toBe("goodbye"); 293 | expect(operators.$$if("world",{test:["hello","world","goodbye"]})).toBeUndefined(); 294 | }) 295 | test("$$case",() => { 296 | expect(operators.$$case("hello",{test:["hello","world","goodbye"]})).toBe("world"); 297 | expect(operators.$$case("goodbye",{test:["hello","world","goodbye","world"]})).toBe("world"); 298 | expect(operators.$$case("world",{test:["hello","world","default"]})).toBe("default"); 299 | expect(operators.$$case("world",{test:["hello","world"]})).toBeUndefined() 300 | }) 301 | test("$$concat",() => { 302 | expect(operators.$$concat("hello",{test:"world"})).toBe("helloworld"); 303 | expect(operators.$$concat([1],[2])).toEqual([1,2]); 304 | expect(operators.$$concat("hello",{test:1})).toBe("hello1"); 305 | expect(operators.$$concat("hello",{test:[1,2]})).toBe("hello1,2"); 306 | expect(operators.$$concat("hello",{test:{a:1}})).toBe("hello[object Object]"); 307 | expect(operators.$$concat("hello",{test:undefined})).toBe("helloundefined"); 308 | expect(operators.$$concat(1,{test:1})).toBe("11"); 309 | }) 310 | test("$$join",() => { 311 | expect(operators.$$join("hello",{test:"world"})).toBe("helloworld"); 312 | expect(operators.$$join([1],[2])).toEqual([1,2]); 313 | expect(operators.$$join("hello",{test:1})).toBe("hello1"); 314 | expect(operators.$$join("hello",{test:[1,2]})).toBe("hello1,2"); 315 | expect(operators.$$join("hello",{test:{a:1}})).toBe("hello[object Object]"); 316 | expect(operators.$$join("hello",{test:undefined})).toBe("helloundefined"); 317 | expect(operators.$$join(1,{test:1})).toBe("11"); 318 | }) 319 | test("$$slice",() => { 320 | expect(operators.$$slice([1,2,3,4],{test:[1]})).toEqual([2,3,4]); 321 | expect(operators.$$slice([1,2,3,4],{test:[1,2]})).toEqual([2,3]); 322 | expect(operators.$$slice("hello",{test:1})).toBe("ello"); 323 | expect(operators.$$slice("hello",{test:[1,3]})).toBe("el"); 324 | expect(operators.$$slice(1,{test:[1]})).toBeUndefined() 325 | }) 326 | test("$$substring",() => { 327 | expect(operators.$$substring("hello",{test:[1]})).toBe("ello"); 328 | expect(operators.$$substring("hello",{test:[1,3]})).toBe("el"); 329 | expect(operators.$$substring(1,{test:[1]})).toBeUndefined() 330 | }) 331 | */ 332 | 333 | 334 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import {ANY,selector} from "lmdb-index"; 2 | import cartesianProduct from "@anywhichway/cartesian-product"; 3 | import {operators} from "./src/operators.js"; 4 | 5 | 6 | const optimize = (conditions,classNames) => { 7 | // optimize each to level portion 8 | // to do sort top level portions 9 | return Object.entries(conditions).reduce((optimized,[alias,condition]) => { 10 | const optimalOrder = Object.entries(condition).sort(([key1,value1],[key2,value2]) => { 11 | if(value1===null) { 12 | return value1===value2 ? 0 : -1 13 | } 14 | if(value2===null) { 15 | return value1===value2 ? 0 : 1 16 | } 17 | const type1 = typeof(value1), 18 | type2 = typeof(value2); 19 | if(type1==="object") { 20 | return type2=="object" ? 0 : 1 21 | } 22 | if(type1==="function") { 23 | return type2==="function" ? 0 : type2==="object" ? -1 : 1; 24 | } 25 | if(type1===type2) { 26 | return value1 > value2 ? 1 : value1 === value2 ? 0 : -1 27 | } 28 | if(type1==="symbol") return -1; 29 | if(type2==="symbol") return 1; 30 | return type1 < type2 ? -1 : 1; 31 | }) 32 | optimized[alias] = optimalOrder.reduce((conditions,[key,value]) => { 33 | conditions[key] = value; 34 | return conditions; 35 | },{}) 36 | return optimized; 37 | },{}) 38 | } 39 | 40 | function compileClasses (db,...classes) { 41 | return classes.reduce((result,item) => { 42 | const [cls,name] = Array.isArray(item) ? item : [item], 43 | cname = cls.name, 44 | index = "@@" + cname; 45 | db.defineSchema(cls); 46 | result[name||cname] = { 47 | cname, 48 | *entries(property,value) { 49 | const type = typeof(value); 50 | if(type==="string" || type==="number" || type==="boolean") { 51 | for(const {key} of db.getRange([property,value,index])) { 52 | if(key.length===4) { 53 | const id = key[key.length-1], 54 | value = db.get(id); 55 | if(value!==undefined) { 56 | yield {key:id,value} 57 | } 58 | } 59 | } 60 | } else { 61 | for(const {key} of db.getRangeFromIndex({[property]:value},null,null,{cname})) { 62 | const value = db.get(key); 63 | if(value!==undefined) { 64 | yield {key,value} 65 | } 66 | } 67 | } 68 | } 69 | } 70 | return result; 71 | },{}); 72 | } 73 | 74 | function* where(db,conditions={},classes,select,coerce) { 75 | const results = {}; 76 | // {$t1: {name: {$t2: {name: (value) => value!=null}} or null is a function for testing 77 | const aliases = new Set(); 78 | for(const [leftalias,leftpattern] of Object.entries(conditions)) { 79 | const cname = classes[leftalias]?.cname, 80 | idprefix = cname ? cname + "@" : /.*\@/g, 81 | schema = db.getSchema(cname), 82 | idkey = schema?.idKey || "#"; 83 | aliases.add(leftalias); 84 | results[leftalias] ||= {}; 85 | let maxCount = 0; 86 | for(const [leftproperty,test] of Object.entries(leftpattern)) { 87 | const type = typeof(test); 88 | let generator 89 | if(test && type==="object") { // get all instances of @ 90 | generator = db.getRange({start:[idprefix]}); 91 | } else { // get ids of all instances. relies on index structure created by lmdb-index, could use getRangeWhere instead, but less efficient 92 | generator = db.getRangeFromIndex({[leftproperty]:test}) 93 | } 94 | for(let {value} of generator) { 95 | const id = value[schema.idKey]; 96 | if(!id.startsWith(idprefix) && !(typeof(idPrefix)==="object" && idprefix instanceof RegExp && !id.match(idprefix))) { 97 | break; 98 | } 99 | let leftvalue = value[leftproperty]; 100 | results[leftalias][id] ||= {count:0,value}; 101 | maxCount = results[leftalias][id].count += 1; 102 | if(test && type==="object") { 103 | for (const [rightalias, rightpattern] of Object.entries(test)) { 104 | results[rightalias] ||= {}; 105 | aliases.add(rightalias); 106 | let every = true; 107 | for (const [rightproperty, rightvalue] of Object.entries(rightpattern)) { 108 | const type = typeof (rightvalue); 109 | // fail if property value test fails 110 | if ((type === "function" && rightvalue.length === 1 && rightvalue(leftvalue) === undefined) || (type !== "function" && rightvalue !== leftvalue)) { 111 | every = false; 112 | break; 113 | } 114 | // gets objects with same property where property values match 115 | const test = type === "function" ? ANY : leftvalue; // get all values for property 116 | for (const { 117 | key, 118 | value 119 | } of classes[rightalias].entries(rightproperty, test)) { 120 | // fail if comparison function fails 121 | if (type === "function" && rightvalue.length > 1 && (rightvalue.callRight ? rightvalue.callRight(leftvalue, value[rightproperty]) : rightvalue(leftvalue, value[rightproperty])) === undefined) { 122 | delete results[rightalias][key]; 123 | break; 124 | } 125 | results[rightalias][key] ||= {value}; 126 | } 127 | } 128 | if (!every) { 129 | delete results[rightalias]; 130 | break; 131 | } 132 | } 133 | } 134 | } 135 | if(maxCount===0) { 136 | delete results[leftalias]; 137 | break; 138 | } 139 | } 140 | if(maxCount===0) { 141 | delete results[leftalias]; 142 | break; 143 | } 144 | for(const [id, {count}] of Object.entries(results[leftalias])) { 145 | if(count alias in results)) { 151 | return; 152 | } 153 | const names = Object.keys(results); 154 | for(const classValues of Object.values(results)) { 155 | for (const product of cartesianProduct(Object.keys(classValues))) { // get all combinations of instance ids for each class 156 | const join = {}; 157 | product.forEach((id, i) => { 158 | const name = names[i]; 159 | join[name] = results[name][id].value; 160 | }) 161 | const selected = select ? (select === IDS ? select.bind(db)(join) : selector(join,select)) : join; 162 | if (selected != undefined) { 163 | yield selected; 164 | } 165 | } 166 | } 167 | } 168 | 169 | function del() { 170 | const db = this; 171 | return { 172 | from(...classes) { 173 | classes = compileClasses(db,...classes); 174 | async function *_where(conditions={}) { 175 | for(const join of where(db,conditions,classes,IDS)) { 176 | for(const id of join) { 177 | await db.remove(id); 178 | yield id; 179 | } 180 | } 181 | } 182 | return { 183 | where(conditions={}) { 184 | let generator = _where(optimize(conditions,classes)); 185 | const exec = async () => { 186 | const items = []; 187 | for await (const item of generator) { 188 | items.push(item); 189 | } 190 | generator = _where(optimize(conditions,classes)); 191 | generator.exec = exec; 192 | return items; 193 | } 194 | generator.exec = exec; 195 | return generator; 196 | } 197 | } 198 | } 199 | } 200 | } 201 | 202 | function insert() { 203 | const db = this; 204 | return { 205 | into(...classes) { 206 | classes = compileClasses(db,...classes); 207 | async function* _values(values) { 208 | for(let [key,instances] of Object.entries(values)) { 209 | const {cname} = classes[key], 210 | schema = db.getSchema(cname); 211 | if(instances instanceof Array) { 212 | if(!(instances[0] instanceof Array) && schema.create([]) instanceof Array) { 213 | throw new TypeError("Expected array of arrays when inserting Array"); 214 | } 215 | } else { 216 | instances = [instances]; 217 | } 218 | for(let instance of instances) { 219 | if(!(instance instanceof schema.ctor)) { 220 | instance = schema.create(instance); 221 | } 222 | yield await db.put(null,instance); 223 | } 224 | } 225 | } 226 | return { 227 | values(values) { 228 | let generator = _values(values); 229 | const exec = async () => { 230 | const ids = []; 231 | for await(const id of generator) { 232 | ids.push(id); 233 | } 234 | generator = _values(values); 235 | generator.exec = exec; 236 | return ids; 237 | }; 238 | generator.exec = exec; 239 | return generator; 240 | } 241 | } 242 | } 243 | } 244 | } 245 | 246 | function select(select) { 247 | const db = this; 248 | return { 249 | from(...classes) { 250 | classes = compileClasses(db,...classes); 251 | function *_where(conditions={}) { 252 | for(const item of where(db,conditions,classes,select)) { 253 | yield item; 254 | } 255 | } 256 | return { 257 | where(conditions={}) { 258 | let generator = _where(optimize(conditions,classes)); 259 | const exec = () => { 260 | const items = []; 261 | for (const item of generator) { 262 | items.push(item); 263 | } 264 | generator = _where(optimize(conditions,classes)); 265 | generator.exec = exec; 266 | return items; 267 | } 268 | generator.exec = exec; 269 | return generator; 270 | } 271 | } 272 | } 273 | } 274 | } 275 | 276 | function update(...classes) { 277 | const db = this; 278 | classes = compileClasses(db,...classes); 279 | return { 280 | set(patches) { 281 | async function *_where(conditions={}) { 282 | for(const join of where(db,conditions,classes,IDS)) { 283 | for(const id of join) { 284 | for(const {cname} of Object.values(classes)) { 285 | const patch = patches[cname]; 286 | if(patch && id.startsWith(cname+"@")) { 287 | await db.patch(id,patch); 288 | } 289 | } 290 | yield id; 291 | } 292 | } 293 | } 294 | return { 295 | where(conditions={}) { 296 | let generator = _where(optimize(conditions,classes)); 297 | const exec = async () => { 298 | const items = []; 299 | for await(const item of generator) { 300 | items.push(item); 301 | } 302 | generator = _where(optimize(conditions,classes)); 303 | generator.exec = exec; 304 | return items; 305 | } 306 | generator.exec = exec; 307 | return generator; 308 | } 309 | } 310 | } 311 | } 312 | } 313 | 314 | import {withExtensions as lmdbExtend} from "lmdb-index"; 315 | 316 | const withExtensions = (db,extensions={}) => { 317 | return lmdbExtend(db,{delete:del,insert,select,update,...extensions}) 318 | } 319 | 320 | const functionalOperators = Object.entries(operators).reduce((operators,[key,f]) => { 321 | operators[key] = function(test) { 322 | let join; 323 | const op = (left,right) => { 324 | return join ? f(left,right) : f(left,{test}); 325 | } 326 | op.callRight = (left,right) => { 327 | join = true; 328 | let result; 329 | try { 330 | result = (test===undefined ? op(left, {test:right}) : op(right,{test})); 331 | join = false; 332 | } finally { 333 | join = false; 334 | } 335 | return result; 336 | } 337 | return op; 338 | } 339 | operators.$and = (...tests) => { 340 | const op = (left,right) => { 341 | return tests.every((test) => test(left,right)); 342 | } 343 | op.callRight = (left,right) => { 344 | return tests.every((test) => test.callRight(left,right)); 345 | } 346 | return op; 347 | } 348 | operators.$or = (...tests) => { 349 | const op = (left,right) => { 350 | return tests.some((test) => test(left,right)); 351 | } 352 | op.callRight = (left,right) => { 353 | return tests.every((test) => test.callRight(left,right)); 354 | } 355 | return op; 356 | } 357 | operators.$not = (test) => { 358 | const op = (left,right) => { 359 | return !test(left,right); 360 | } 361 | op.callRight = (left,right) => { 362 | return !test.callRight(left,right); 363 | } 364 | return op; 365 | } 366 | return operators; 367 | },{}); 368 | 369 | function IDS(value) { 370 | return Object.values(value).map((value) => { 371 | const schema = this.getSchema(value); 372 | return schema ? value[schema.idKey||"#"] : value["#"] 373 | }); 374 | 375 | } 376 | 377 | export {functionalOperators as operators,withExtensions,IDS} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lmdb-oql 2 | A high level object query language for indexed [LMDB](https://www.npmjs.com/package/lmdb) databases using [lmdb-index](https://github.com/anywhichway/lmdb-index). 3 | 4 | Because it is layered on top of LMDB, `lmdb-oql` is fast and has ACID properties. It can also be schemaless or schema based in the same database. 5 | 6 | Easily join and select data across multiple collections/classes using literals, functions, regular expressions, and built-in predicates to match property names or values using the familiar nomenclature `select(what).from(classes).where(conditions)`. 7 | 8 | ```javascript 9 | select({Database:{description:$isDefined()},Provider:{name:$isDefined()}}) 10 | .from(Database,Performance,Provider) 11 | .where({Database: 12 | { 13 | repository:{ // https://github.com/anywhichway/lmdb-index 14 | Provider:{provides:$includes()}, // provides property array includes Database repository 15 | Performance:{ 16 | repository:$eq(), // repository property equals Database repository 17 | primitivePutOpsPerSecond:$gte(400000), 18 | primitiveGetOpsPerSecond:$gte(40000000), // yes, forty million (when objects are cached) 19 | indexedPutOpsPerSecond:$gte(175000), // direct object put with indexing 20 | insertOpsPerSecond:$gte(60000), // object insertion via lmdb-oql with indexing 21 | selectOpsPerSecond:$gte(100000) // object selection via lmdb-oql against index 22 | }} 23 | }}); 24 | /* On a i7-1165G7 CPU @ 2.80GHz with 16GB RAM running Windows 11 x64 25 | { 26 | Database: { 27 | description: "A high performance, high reliability, high durability key-value and object store.", 28 | }, 29 | Provider: { 30 | name: "AnyWhichWay, LLC", 31 | } 32 | */ 33 | ``` 34 | 35 | This is BETA software. The API is stable and unit tests have over 90% coverage. 36 | 37 | Note: Schema are currently used for indexing only. They are not used to validate data. This is a planned feature subsequent to v1.0.0. 38 | 39 | # Installation 40 | 41 | ```bash 42 | npm install lmdb-oql 43 | ``` 44 | 45 | # Usage 46 | 47 | ```javascript 48 | import {open} from "lmdb"; 49 | import {operators,withExtensions,IDS} from "lmdb-oql"; 50 | 51 | const {$mod,$gte} = operators; 52 | class Person { 53 | constructor(config={}) { 54 | Object.assign(this,config); 55 | } 56 | } 57 | class Employer { 58 | constructor(config={}) { 59 | Object.assign(this,config); 60 | } 61 | } 62 | 63 | const db = withExtensions(open("test")); 64 | db.clearSync(); 65 | db.defineSchema(Person); 66 | db.defineSchema(Employer); 67 | 68 | // typically you do not provide an id for a put of a an instance controlled by a schema 69 | const personId = await db.put(null,new Person({name:"bill",age:21,employer:"ACME"})); 70 | // but you can if you want to, so long as you start it with the class name followed by @ 71 | await db.put("Employer@1",new Employer({name:"ACME",address:"123 Main St."})); 72 | 73 | const person = await db.get(personId); 74 | // if a return value is an object that is controlled by a schema, 75 | // it will be an instance of the schema's class 76 | console.log(person); 77 | /* 78 | Person { 79 | name: 'bill', 80 | age: 21, 81 | employer: 'ACME', 82 | '#': 'Person@850ad934-a449-493e-846a-96e00a1b6546' 83 | } 84 | */ 85 | 86 | // you can use predefined operators in place of literal matches 87 | console.log([...db.select().from(Person).where({Person:{age:$gte(21)}})]); 88 | /* 89 | [ 90 | { 91 | Person: Person { 92 | name: 'bill', 93 | age: 21, 94 | employer: 'ACME', 95 | '#': 'Person@850ad934-a449-493e-846a-96e00a1b6546' 96 | } 97 | } 98 | ] 99 | */ 100 | // there are lots of operators, Person has an odd numbered age, could also use $odd 101 | console.log([...db.select().from(Person).where({Person:{age:$mod([2,1])}})]); 102 | /* 103 | [ 104 | { 105 | Person: Person { 106 | name: 'bill', 107 | age: 21, 108 | employer: 'ACME', 109 | '#': 'Person@850ad934-a449-493e-846a-96e00a1b6546' 110 | } 111 | } 112 | ] 113 | */ 114 | 115 | // joins are performed using the class name as the key 116 | // this example joins Person to Employer on Person.employer === Employer.name === "ACME 117 | console.log([...db.select().from(Person,Employer).where({Person:{employer: {Employer:{name:"ACME"}}}})]); 118 | /* 119 | [ 120 | { 121 | Person: Person { 122 | name: 'bill', 123 | age: 21, 124 | employer: 'ACME', 125 | '#': 'Person@850ad934-a449-493e-846a-96e00a1b6546' 126 | }, 127 | Employer: Employer { 128 | name: 'ACME', 129 | address: '123 Main St.', 130 | '#': 'Employer@c5e07e94-4a94-4cfc-b167-65cb1dd7bd29' 131 | } 132 | } 133 | ] 134 | */ 135 | 136 | // class aliases are supported by providing two element arrays in 'from' 137 | // with the first element being the class and the second being the alias 138 | console.log([...db.select().from([Person, "P"],[Employer,"E"]).where({P:{employer: {E:{name:"ACME"}}}})]); 139 | /* 140 | [ 141 | { 142 | Person: Person { 143 | name: 'bill', 144 | age: 21, 145 | employer: 'ACME', 146 | '#': 'Person@850ad934-a449-493e-846a-96e00a1b6546' 147 | }, 148 | Employer: Employer { 149 | name: 'ACME', 150 | address: '123 Main St.', 151 | '#': 'Employer@c5e07e94-4a94-4cfc-b167-65cb1dd7bd29' 152 | } 153 | } 154 | ] 155 | */ 156 | 157 | // you can select just the data you want and move it up a level 158 | console.log([...db.select({P:{name(value,{root}) { root.name=value; }},E:{address(value,{root}){ root.workAddress=value; }}}) 159 | .from([Person, "P"],[Employer,"E"]) 160 | .where({P:{employer: {E:{name:"ACME"}}}})]); 161 | /* 162 | [ { name: 'bill', workAddress: '123 Main St.' } ] 163 | */ 164 | 165 | // you can select just ids 166 | console.log([...db.select(IDS).from([Person, "P"],[Employer,"E"]).where({P:{employer: {E:{name:"ACME"}}}})]) 167 | /* 168 | [ [ 'Person@64fc6554-066c-47dd-a99e-d0492dcb957c', 'Employer@1' ] ] 169 | */ 170 | 171 | ``` 172 | 173 | ## Operators 174 | 175 | Operators take either 0 or 1 argument. 176 | 177 | Zero argument operators are typically used to test the type of the value being compared, e.g. `$isZIPCode()`. 178 | 179 | On the right side of a join, providing no argument when one is expected compares the value to the left, e.g. `{Person:{employer: {Employer:{name:$eq()}}}}`. 180 | 181 | Providing 1 argument creates a right outer join where the right side value satisfies the operator, e.g. `{Person:{employer: {Employer:{name:$eq("ACME")}}}}`. 182 | 183 | Note: `$eq()` is provided for completeness, but using a literal is more efficient, e.g. `{Person:{employer: {Employer:{name:"ACME"}}}}`. Using a function causes a partial index scan. 184 | 185 | The documentation below just shows how to use the operator with a single argument. The `item being compared` for each definition below is the value of a property in an object on either the left or right side of a join. 186 | 187 | ### Logical 188 | 189 | `$and(...operatorCalls)` - returns the item being compared if it satisfies all conditions, otherwise `undefined` 190 | 191 | `$or(...operatorCalls)` - returns the item being compared if it satisfies any condition, otherwise `undefined` 192 | 193 | `$not(operatorCall)` - returns the item being compared if it does not satisfy the condition, otherwise `undefined` 194 | 195 | ### Types 196 | 197 | `$type(value:any)` - returns item being compared if it is of type value, otherwise `undefined` 198 | 199 | `$isOdd(value:any)` - returns item being compared if it is odd, otherwise `undefined` 200 | 201 | `$isEven(value:any)` - returns item being compared if it is even, otherwise `undefined` 202 | 203 | `$isTruthy(value:any)` - returns item being compared if it is truthy, otherwise `undefined` 204 | 205 | `$isFalsy(value:any)` - returns item being compared if it is falsy, otherwise `undefined` 206 | 207 | `$isPositive(value:any)` - returns item being compared if it is positive, otherwise `undefined` 208 | 209 | `$isNegative(value:any)` - returns item being compared if it is negative, otherwise `undefined` 210 | 211 | `$isFloat(value:any)` - returns item being compared if it is a float, otherwise `undefined` 212 | 213 | `$isNaN(value:any)` - returns item being compared if it is not a number, otherwise `undefined` 214 | 215 | `$isInteger(value:any)` - returns item being compared if it is an integer, otherwise `undefined` 216 | 217 | `$isUndefined(value:any)` - returns item being compared if it is undefined, otherwise `undefined` 218 | 219 | `$isDefined(value:any)` - returns item being compared if it is defined, otherwise `undefined` 220 | 221 | `$isNull(value:any)` - returns item being compared if it is null, otherwise `undefined` 222 | 223 | `$isPrimitive(value:any)` - returns item being compared if it is a primitive, otherwise `undefined` 224 | 225 | `$isArray(value:any)` - returns item being compared if it is an array, otherwise `undefined` 226 | 227 | `$isEmail(value:any)` - returns item being compared if it is an email address , otherwise `undefined` 228 | 229 | `$isURL(value:any)` - returns item being compared if it is a URL , otherwise `undefined` 230 | 231 | `$isUUID(value:any)` - returns item being compared if it is a v4 UUID , otherwise `undefined` 232 | 233 | `$isISBN(value:any)` - returns item being compared if it is an ISBN number, otherwise `undefined` 234 | 235 | `$isSSN(value:any)` - returns item being compared if it is a social security number, otherwise `undefined` 236 | 237 | `$isZIPCode(value:any)` - returns item being compared if it is a zip code, otherwise `undefined` 238 | 239 | ### Comparisons 240 | 241 | `$lt(value:number|string)` - returns item being compared if it is < value, otherwise `undefined` 242 | 243 | `$lte(value:number|string)` - returns item being compared if it is <= value, otherwise `undefined` 244 | 245 | `$eq(value:number|string)` - returns item being compared if it is == value, otherwise `undefined` 246 | 247 | `$eeq(value:number|string)` - returns item being compared if it is === value, otherwise `undefined 248 | 249 | `$neq(value:number|string)` - returns item being compared if it is != value, otherwise `undefined` 250 | 251 | `$gte(value:number|string)` - returns item being compared if it is >= value, otherwise `undefined` 252 | 253 | `$gt(value:number|string)` - returns item being compared if it is > value, otherwise `undefined` 254 | 255 | `$between(value1:number|string,value2:number|string)` - returns item being compared if it is >= value1 and <= value2 , otherwise `undefined` 256 | 257 | `$outside(value1:number|string,value2:number|string)` - returns item being compared if it is < value1 or > value2 , otherwise `undefined` 258 | 259 | ### Arrays & Strings 260 | 261 | `$in(value:array|string)` - returns item being compared if it is in value, otherwise `undefined` 262 | 263 | `$nin(value:array|string)` - returns item being compared if it is not in value, otherwise `undefined` 264 | 265 | `$includes(value:any)` - returns item being compared if it contains value, otherwise `undefined` 266 | 267 | `$excludes(value:any)` - returns item being compared if it does not contain value, otherwise `undefined` 268 | 269 | `$in(value:array|string)` - returns item being compared if it is in value, otherwise `undefined` 270 | 271 | `$nin(value:array|string)` - returns item being compared if it is not in value, otherwise `undefined` 272 | 273 | `$startsWith(value:string)` - returns item being compared if it starts with value, otherwise `undefined` 274 | 275 | `$endsWith(value:string)` - returns item being compared if it ends with value, otherwise `undefined` 276 | 277 | `$length(value:number)` - returns item being compared if it has length value, otherwise `undefined` 278 | 279 | ### Sets (based on Arrays) 280 | 281 | `$intersects(value:array)` - returns item being compared if it intersects with value, otherwise `undefined` 282 | 283 | `$disjoint(value:array)` - returns item being compared if it does not intersect with value, otherwise `undefined` 284 | 285 | `$subset(value:array)` - returns item being compared if it is a subset of value, otherwise `undefined` 286 | 287 | `$superset(value:array)` - returns item being compared if it is a superset of value, otherwise `undefined` 288 | 289 | `$symmetric(value:array)` - returns item being compared if it is a symmetric of value, otherwise `undefined` 290 | 291 | ### Other String Operators 292 | 293 | `$matches(regexp:RegExp)` - returns item being compared if it matches `regexp`, otherwise `undefined` 294 | 295 | `$echoes(value:string)` - returns item being compared if it sounds like `value`, otherwise `undefined` 296 | 297 | ### Math 298 | 299 | `$odd(value)` - returns item being compared if it is odd , otherwise `undefined` 300 | 301 | `$even(value)` - returns item being compared if it is even , otherwise `undefined` 302 | 303 | `add(value:array)` - returns item being compared if item being compared + value[0] === value[1] , otherwise `undefined` 304 | 305 | `subtract(value:array)` - returns item being compared if item being compared - value[0] === value[1] , otherwise `undefined` 306 | 307 | `multiply(value:array)` - returns item being compared if item being compared * value[0] === value[1] , otherwise `undefined` 308 | 309 | `divide(value:array)` - returns item being compared if item being compared / value[0] === value[1] , otherwise `undefined` 310 | 311 | `$mod(value:array)` - returns item being compared if the mod of value[0] is value[1] , otherwise `undefined` 312 | 313 | ### LMDB Index API 314 | 315 | Developers should be familiar with the behavior of [lmdb-index](https://github.com/anywhichway/lmdb-index), particularly `defineSchema` and `put`, the documentation for which is replicated here: 316 | 317 | ### async defineSchema(classConstructor,?options={}) - returns boolean 318 | 319 | - The key names in the array `options.indexKeys` will be indexed. If no value is provided, all keys will be indexed. If `options.indexKeys` is an empty array, no keys will be indexed. 320 | - If the property `options.idKey` is provided, its value will be used for unique ids. If `options.idKey` is not provided, the property `#` on instances will be used for unique ids. 321 | - If the property `options.keyGenerator` is provided, its value should be a function that returns a unique id. This will be prefixed by `@`. If `options.keyGenerator` is not provided, a v4 UUID will be used. 322 | 323 | The `options` properties and values are inherited by child schema, i.e. if you define them for `Object`, then you do not need to provide them for other classes. 324 | 325 | To index all keys on all objects using UUIDs as ids and `#` as the id key, call `db.defineSchema(Object)`. 326 | 327 | ### async db.put(key,value,?version,?ifVersion) - returns boolean 328 | 329 | Works similar to [lmdb put](https://github.com/kriszyp/lmdb-js#dbputkey-value-version-number-ifversion-number-promiseboolean) 330 | 331 | If `value` is an object, it will be indexed by the top level keys of the object so long as it is an instance of an object controlled by a schema declared with `defineSchema`. To index all top level keys on all objects, call `db.defineSchema(Object)`. If `key` is `null`, a unique id will be generated and added to the object. See [defineSchema](#async-defineschemaclassconstructor-options) for more information. 332 | 333 | If there is a mismatch between the `key` and the `idKey` of the object, an Error will be thrown. 334 | 335 | ***Note***: For objects to be retrievable using `lmdb-oql`, the assignment of keys to objects ***MUST*** be done by calling `put` with `null or keys ***MUST*** be of the form `@` 336 | 337 | # API 338 | 339 | The `select` method is documented first because it illustrates the full range of argument surfaces also used in `delete`, `insert`, and `update`. After `select`, methods are documented in alphabetical order. 340 | 341 | `* db.select(?selector:object|function,?{class:constructor}).from(...classes).where(?conditions:object)` - yields object representing join 342 | 343 | A generator function that selects instances across multiple classes based on a `conditions` object that can contain literals, regular expressions, functions, and joins. The `where` chained function optimizes the `conditions` and processes the most restrictive criteria first. 344 | 345 | The yielded value will be a class instance if only one class is provided in `from` and plain object if the selection is done across multiple classes unless a `class` is provided to `selector`. A `class` provided to `select` and used in `from` does not have to be declared using `defineSchema`, it will be automatically declared and all top level properties will be indexed. If a schema has already been defined, it will be respected. The objects nested in the `conditions` object (see `where` below) do not have to be instances of their respective classes, it is their data values that matter. 346 | 347 | `selector(result:object)` - If selector is a function it takes an object representing the joined instances matching the `where` object. It can manipulate the object in any way it wishes. It defaults to `(value) => value` 348 | 349 | `selector:object` - If `selector` is an object, its properties can be functions or regular expressions that extract and manipulate values from the result joined instances matching the `where` object. 350 | 351 | `...classes` can be any number of class constructors. By default, the constructor name will be used as an alias in the `where` clause and joined instances. Specific alias can be provided by using two element arrays, e.g. `from([Person,"P1"],[Person,"P2"])` if you need to join `Person` back to itself. 352 | 353 | `where` is an onbject with top level properties matching class names or aliases. The values of these properties are objects used to match against instances of the classes. These sub-objects contain literals, RegExp, and functions as property values. Serialized regular expressions can also be used for property names. Joins are created by using a class alias (see `...classes` above) as a property name. 354 | 355 | For example: 356 | 357 | ```javascript 358 | db.select() 359 | .from(Person) 360 | .where({Person:{name:"joe"}}) // will yield all Person objects {Person: {name:"joe",...otherProperties}} 361 | ``` 362 | 363 | ```javascript 364 | db.select() 365 | .from(Person) 366 | .where({Person:{name:(value)=>value==="joe" ? value : undefined}}) // yields the same results 367 | ``` 368 | 369 | ```javascript 370 | db.select() 371 | .from(Person) 372 | .where({Person:{[/name/g]:(value)=>value==="joe" ? value : undefined}}) // yields the same results 373 | ``` 374 | 375 | ```javascript 376 | db.select() 377 | .from(Person) 378 | .where({Person:{[/name/g]:/joe/g}}) // yields the same results 379 | ``` 380 | 381 | ```javascript 382 | db.select() 383 | .from([Person,"P1"]) 384 | .where({P1:{[/name/g]:"joe"}}) ///yields objects of the form {P1: {name:"joe",...otherProperties}} 385 | ``` 386 | 387 | ```javascript 388 | db.select() 389 | .from([Person,"P1"],[Person,"P2"]) 390 | .where({}) // yields objects of the form {P1: {name:"joe",...otherProperties}, P2:{name:"joe",...}} 391 | ``` 392 | 393 | ```javascript 394 | db.select({Person:{name(value,{root}) { delete root.Person;return root.name=value; }}}) 395 | .from(Person) 396 | .where({name:NOTNULL}) // yields objects of the form {name:} 397 | ``` 398 | 399 | `* async db.delete().from(...classes:class).where(?conditions:object)` - yields ids of deleted objects 400 | 401 | Deletes instances of `classes` based on `conditions`. If `conditions` is not provided, all instances of `classes` will be deleted. 402 | 403 | All the deletions are done in a single transaction. 404 | 405 | Attempting to use `.db.get()` on a deleted object will return `undefined`. The `ids` are just yielded for convenience. 406 | 407 | `* async db.insert().into(...classes:class).values(values:object)` - yields ids of inserted objects as an array 408 | 409 | Multiple `classes` can be provided. If they do not have a schema defined, one will be defined automatically. This is the primary advantage of using `insert` over `put` with a null key. 410 | 411 | The `values` object should contain top level properties that match the class names or aliases of the `classes` provided. The values of those properties should be objects containing the properties to be inserted,e g. `{Person:{name:"Bill",age:22}}` will insert a `Person` with name "Bill" and age of 22. You can also provide an array of objects to insert multiple objects, e.g. `{Person:[{name"Mary",age:22},{name:"John",age:23}]}` will insert two `Person` objects. If you need to store Arrays as top level database objects, you can use the `Array` class and MUST provide an array with more than one dimension as a value, e.g. `{Array:[[1,2,3],[4,5,6]]}` will insert `Array` objects. 412 | 413 | All the inserts are done in a single transaction. 414 | 415 | `* async db.put(key,?data,?options)` - yields id of inserted object 416 | 417 | Effectively the same as `db.put(null,data)` except that `data` can be a plain object and is coerced into and instance of a class. 418 | 419 | `* async db.update(...classes:class).set(updates:object).where(?conditions:object)` - yields ids of updated objects 420 | 421 | Patches instances of `classes`with `updates` based on `conditions`. If `conditions` is not provided, all instances of `classes` will be updated. 422 | 423 | Multiple `classes` can be provided. The `updates` object should contain top level properties that match the class names of the `classes` provided. The values of those properties should be objects containing the properties to be updated and their new values,e g. `{Person:{age:22}}` will update all matching instances of `Person` to have the age of 22. 424 | 425 | All the updates are done in a single transaction. 426 | 427 | Providing property values of `undefined` in `updates will delete the property from instances. 428 | 429 | 430 | # Testing 431 | 432 | Testing conducted with `jest`. 433 | 434 | File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 435 | ----------|---------|----------|---------|---------|------------------------ 436 | All files | 90.09 | 84.24 | 94.78 | 91.12 | 437 | lmdb-oql | 86.58 | 67.62 | 88.46 | 88.39 | 438 | index.js | 86.58 | 67.62 | 88.46 | 88.39 | ...12,125-126,136-137,143-144,165,182,212,280,315,356,365,374 439 | lmdb-oql/src | 100 | 97.67 | 100 | 100 | 440 | operators.js | 100 | 97.67 | 100 | 100 | 10,167,171-172 441 | 442 | # Release Notes (Reverse Chronological Order) 443 | 444 | During ALPHA and BETA, the following semantic versioning rules apply: 445 | 446 | * The major version will be zero. 447 | * Breaking changes or feature additions will increment the minor version. 448 | * Bug fixes and documentation changes will increment the patch version. 449 | 450 | 451 | 2023-11-26 v0.5.8 Updated test suite to ensure operation with child databases. 452 | 453 | 2023-06-01 v0.5.7 Integrated v1.0.0 release of `lmdb-index`. Removed `lmdb-query` and `array-set-ops` as dependencies. 454 | 455 | 2023-05-26 v0.5.6 Corrected respository pointer in package.json. 456 | 457 | 2023-05-04 v0.5.5 Documentation corrections. 458 | 459 | 2023-05-04 v0.5.4 Updated dependencies. 460 | 461 | 2023-05-04 v0.5.3 Documentation enhancements. Fixed issues related to undocumented `.exec()` functions. Improved performance of `select`. Added some unit tests. 462 | 463 | 2023-05-03 v0.5.2 Documentation enhancements. Fixed issues related to complex joins and nested object matches not returning results. Added some performance testing and did a little optimization. Ensured all `oql` database changes are wrapped in transactions. Unit test coverage has degraded from 96% to 90% due to the addition of some code. 464 | 465 | 2023-05-02 v0.5.1 Documentation typo fixes. 466 | 467 | 2023-05-02 v0.5.0 Implemented `delete`, `update`, `insert`, `$and`, `$or`, `$not`. Added unit tests and updated documentation. 468 | API is now stable. Unit tests are now over 90% coverage. Moving to BETA. 469 | 470 | 2023-05-01 v0.4.0 Improved/optimized join capability. Added many operators. Enhanced unit tests. Enhanced documentation. 471 | 472 | 2023-04-30 v0.3.0 Implemented ability to return `IDS` only for `select`. Added lots of operators. Enhanced documentation. 473 | 474 | 2023-04-29 v0.2.1 Enhanced documentation and `examples/basic.js`. 475 | 476 | 2023-04-28 v0.2.0 Implemented operator support. Updated dependencies. 477 | 478 | 2023-04-27 v0.1.0 Enhanced documentation. Re-implemented `where` to be more efficient. 479 | 480 | 2023-04-27 v0.0.1 Initial public release. 481 | 482 | # License 483 | 484 | This software is provided as-is under the [MIT license](http://opensource.org/licenses/MIT). 485 | 486 | Copyright (c) 2023, AnyWhichWay, LLC and Simon Y. Blackwell. 487 | --------------------------------------------------------------------------------