@cnrs/hel 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -9
- package/package.json +2 -2
- package/src/ast2str.js +108 -0
- package/src/grammar.js +19 -1
- package/src/index.js +19 -0
- package/src/str2ast.js +19 -2
- package/src/str2fun.js +29 -8
- package/src/tokens.js +21 -1
package/README.md
CHANGED
|
@@ -2,25 +2,34 @@
|
|
|
2
2
|
|
|
3
3
|
<a href='https://datasphere.readthedocs.io/projects/hel/'><img src='https://gitlab.huma-num.fr/datasphere/doc/assets/-/raw/main/banners/HEL.png' width='100%'></a>
|
|
4
4
|
|
|
5
|
-
](https://www.gnu.org/licenses/agpl-3.0.html)
|
|
6
|
+
[](https://www.repostatus.org/#wip)
|
|
7
|
+
[](https://gitlab.huma-num.fr/datasphere/hel/js/pipelines/latest)
|
|
8
|
+
[](https://datasphere.gitpages.huma-num.fr/hel/js/coverage/)
|
|
9
|
+
[](https://www.npmjs.com/package/@cnrs/hel)
|
|
10
|
+
[](https://datasphere.gitpages.huma-num.fr/hel/js/doc/)
|
|
11
|
+
[](https://doi.org/10.5281/zenodo.18413436)
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
## What is this?
|
|
16
16
|
|
|
17
|
-
**Hel**
|
|
17
|
+
**Hel** makes easier to query databases in a generic way.
|
|
18
|
+
In practice, **Hel** is both the specification of a very simple query language, as long as a JavaScript module that allows you to:
|
|
19
|
+
|
|
20
|
+
- convert a query string to the corresponding [AST (Abstract Syntax Tree)](https://en.wikipedia.org/wiki/Abstract_syntax_tree) ;
|
|
21
|
+
- convert a query string to the corresponding filtering function, to then use it for example with [Heimdall](https://datasphere.readthedocs.io/projects/heimdall/) ;
|
|
22
|
+
- convert an AST to the corresponding query string.
|
|
23
|
+
|
|
24
|
+
For example, combined with [Heimdall.js](https://gitlab.huma-num.fr/datasphere/heimdall/js), **Hel.js** allows you to build web applications that query [Hera](https://datasphere.readthedocs.io/projects/hera/) databases by abstracting both the data retrieval and the data querying details.
|
|
25
|
+
… Which is what [Hecate](https://datasphere.readthedocs.io/projects/hera/) does, by the way.
|
|
18
26
|
|
|
19
27
|
|
|
20
28
|
|
|
21
29
|
## Why should I use it?
|
|
22
30
|
|
|
23
|
-
|
|
31
|
+
Well really your shouldn't, or Hel may drain your soul and take it along the twelve rivers all the way to the Halls of the Dead for an eternity of suffering.
|
|
32
|
+
Anyways, it's up to you. ¯\\_(ツ)_/¯
|
|
24
33
|
|
|
25
34
|
|
|
26
35
|
|
|
@@ -33,6 +42,16 @@ pnpm install @cnrs/hel
|
|
|
33
42
|
```
|
|
34
43
|
You can add it as a dependency to your `package.json` file, as usual.
|
|
35
44
|
|
|
45
|
+
Once installed, you can start using it with a few simple calls:
|
|
46
|
+
```
|
|
47
|
+
import { * as hel } from '@cnrs/hel';
|
|
48
|
+
|
|
49
|
+
const query = '!(type != "cat") & (subtype == "bobcat" | danger > "high")';
|
|
50
|
+
const filter = hel.str2fun(query); // filter function
|
|
51
|
+
const tree = hel.str2ast(query); // Abstract Syntax Tree (AST)
|
|
52
|
+
const equivalent_query = hel.ast2str(tree); // equivalent to query
|
|
53
|
+
```
|
|
54
|
+
|
|
36
55
|
|
|
37
56
|
|
|
38
57
|
## Is it documented?
|
package/package.json
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"https://medium.com/sapioit/why-having-3-numbers-in-the-version-name-is-bad-92fc1f6bc73c",
|
|
29
29
|
"https://gist.github.com/jashkenas/cbd2b088e20279ae2c8e"
|
|
30
30
|
],
|
|
31
|
-
"version": "0.
|
|
31
|
+
"version": "0.4.0",
|
|
32
32
|
"keywords": [
|
|
33
33
|
"hera",
|
|
34
34
|
"hecate",
|
|
@@ -144,4 +144,4 @@
|
|
|
144
144
|
"lint": "exec eslint \"src/**/*.js\"",
|
|
145
145
|
"doc:html": "./node_modules/jsdoc/jsdoc.js -c .jsdoc.conf.json"
|
|
146
146
|
}
|
|
147
|
-
}
|
|
147
|
+
}
|
package/src/ast2str.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
export const KEYS = {
|
|
4
|
+
'pid': property,
|
|
5
|
+
'eid': entity,
|
|
6
|
+
'aid': attribute,
|
|
7
|
+
'not': not,
|
|
8
|
+
'and': and,
|
|
9
|
+
'or': or,
|
|
10
|
+
'operator': relation,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function visit(ast, parentheses=false) {
|
|
14
|
+
for (const [key, visit_] of Object.entries(KEYS)) {
|
|
15
|
+
if (key in ast) {
|
|
16
|
+
return visit_(ast, parentheses);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
throw new Error(`${JSON.stringify(ast)} is invalid`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function property(ast) {
|
|
23
|
+
validate(ast, ['pid', ]);
|
|
24
|
+
return `pid ${ast.pid}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function entity(ast) {
|
|
28
|
+
validate(ast, ['eid', ]);
|
|
29
|
+
return `eid ${ast.eid}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function attribute(ast) {
|
|
33
|
+
validate(ast, ['aid', ]);
|
|
34
|
+
return `aid ${ast.aid}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function not(ast) {
|
|
38
|
+
validate(ast, ['not', ]);
|
|
39
|
+
return `!(${visit(ast.not)})`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function and(ast) {
|
|
43
|
+
validate(ast, ['and', ]);
|
|
44
|
+
const operands = [];
|
|
45
|
+
for (const operand of ast.and) {
|
|
46
|
+
operands.push(visit(operand, true));
|
|
47
|
+
}
|
|
48
|
+
return `${operands.join(' & ')}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function or(ast, parentheses=false) {
|
|
52
|
+
validate(ast, ['or', ]);
|
|
53
|
+
const operands = [];
|
|
54
|
+
for (const operand of ast.or) {
|
|
55
|
+
operands.push(visit(operand));
|
|
56
|
+
}
|
|
57
|
+
const result = `${operands.join(' | ')}`;
|
|
58
|
+
return (parentheses && (result.length > 0)) ? `(${result})` : result;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function relation(ast) {
|
|
62
|
+
validate(ast, ['left', 'operator', 'right']);
|
|
63
|
+
const left = atom(ast.left);
|
|
64
|
+
const right = atom(ast.right);
|
|
65
|
+
return `${left} ${ast.operator} ${right}`
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function atom(ast) {
|
|
69
|
+
if (typeof ast === 'object') {
|
|
70
|
+
return isPropertyOrAttribute(ast);
|
|
71
|
+
}
|
|
72
|
+
if (typeof ast === 'string') {
|
|
73
|
+
return `"${ast}"`;
|
|
74
|
+
}
|
|
75
|
+
return ast.toString();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function isPropertyOrAttribute(ast) {
|
|
79
|
+
if ('pid' in ast) {
|
|
80
|
+
return property(ast);
|
|
81
|
+
} else
|
|
82
|
+
if ('aid' in ast) {
|
|
83
|
+
return attribute(ast);
|
|
84
|
+
} else {
|
|
85
|
+
const dump = JSON.stringify(ast);
|
|
86
|
+
throw new Error(`${dump} is invalid (expected key: 'aid' or 'pid')`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
function validate(ast, keys) {
|
|
92
|
+
for (const key of keys) {
|
|
93
|
+
if (!(key in ast)) {
|
|
94
|
+
const dump = JSON.stringify(ast);
|
|
95
|
+
throw new Error(`Missing key in ${dump} (expected: '${key}')`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (Object.keys(ast).length > keys.length) {
|
|
99
|
+
const dump = JSON.stringify(ast);
|
|
100
|
+
const mine = keys.join(',')
|
|
101
|
+
throw new Error(`Unknown keys in ${dump} (expected only: ${mine})`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
export function toString(ast) {
|
|
107
|
+
return visit(ast);
|
|
108
|
+
}
|
package/src/grammar.js
CHANGED
|
@@ -36,6 +36,7 @@ export class Parser extends CstParser {
|
|
|
36
36
|
this.SUBRULE(this.statement);
|
|
37
37
|
this.CONSUME(tokens.P_RIGHT);
|
|
38
38
|
}},
|
|
39
|
+
{ ALT: () => this.SUBRULE(this.entity) },
|
|
39
40
|
]);
|
|
40
41
|
});
|
|
41
42
|
|
|
@@ -52,7 +53,8 @@ export class Parser extends CstParser {
|
|
|
52
53
|
this.OR([
|
|
53
54
|
{ ALT: () => this.CONSUME(tokens.Integer) },
|
|
54
55
|
{ ALT: () => this.CONSUME(tokens.String) },
|
|
55
|
-
{ ALT: () => this.
|
|
56
|
+
{ ALT: () => this.SUBRULE(this.attribute) },
|
|
57
|
+
{ ALT: () => this.SUBRULE(this.property) },
|
|
56
58
|
]);
|
|
57
59
|
});
|
|
58
60
|
|
|
@@ -67,6 +69,22 @@ export class Parser extends CstParser {
|
|
|
67
69
|
]);
|
|
68
70
|
});
|
|
69
71
|
|
|
72
|
+
this.RULE('entity', () => {
|
|
73
|
+
this.CONSUME(tokens.EID);
|
|
74
|
+
this.CONSUME(tokens.Identifier);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
this.RULE('attribute', () => {
|
|
78
|
+
this.CONSUME(tokens.AID);
|
|
79
|
+
this.CONSUME(tokens.Identifier);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
this.RULE('property', () => {
|
|
83
|
+
this.OPTION(() => { this.CONSUME(tokens.PID); });
|
|
84
|
+
this.CONSUME(tokens.Identifier);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
|
|
70
88
|
// very important to call this after all the rules have been defined.
|
|
71
89
|
// otherwise the parser may not work correctly as it will lack information
|
|
72
90
|
// derived during the self analysis phase.
|
package/src/index.js
CHANGED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { toString as ast2str } from './ast2str.js';
|
|
2
|
+
import { toAst as str2ast } from './str2ast.js';
|
|
3
|
+
import { toFunction as str2fun } from './str2fun.js';
|
|
4
|
+
|
|
5
|
+
import { operators as ops } from './tokens.js';
|
|
6
|
+
|
|
7
|
+
const operators = { };
|
|
8
|
+
for (const [key, token] of Object.entries(ops)) {
|
|
9
|
+
// convert pattern to string and remove surrounding slashes '/'
|
|
10
|
+
const operator = token.PATTERN.toString();
|
|
11
|
+
operators[key] = operator.substring(1, operator.length-1);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export {
|
|
15
|
+
ast2str,
|
|
16
|
+
str2ast,
|
|
17
|
+
str2fun,
|
|
18
|
+
operators,
|
|
19
|
+
};
|
package/src/str2ast.js
CHANGED
|
@@ -52,6 +52,9 @@ class StringToAstVisitor extends CstVisitor {
|
|
|
52
52
|
not: this.visit(context.statement),
|
|
53
53
|
};
|
|
54
54
|
} // else
|
|
55
|
+
if (context.entity) {
|
|
56
|
+
return this.visit(context.entity);
|
|
57
|
+
} // else
|
|
55
58
|
return this.visit(context.statement);
|
|
56
59
|
}
|
|
57
60
|
|
|
@@ -65,9 +68,10 @@ class StringToAstVisitor extends CstVisitor {
|
|
|
65
68
|
|
|
66
69
|
atom(context) {
|
|
67
70
|
// note: these visitor methods will return a string (xxx.image)
|
|
71
|
+
if (context.attribute) return this.visit(context.attribute);
|
|
72
|
+
if (context.property) return this.visit(context.property);
|
|
68
73
|
if (context.Integer) return parseInt(context.Integer[0].image);
|
|
69
|
-
|
|
70
|
-
return context.Identifier[0].image;
|
|
74
|
+
return context.String[0].image;
|
|
71
75
|
}
|
|
72
76
|
|
|
73
77
|
relationalOperator(context) {
|
|
@@ -79,6 +83,19 @@ class StringToAstVisitor extends CstVisitor {
|
|
|
79
83
|
if (context.NotEqual) return context.NotEqual[0].image;
|
|
80
84
|
return context.Equal[0].image;
|
|
81
85
|
}
|
|
86
|
+
|
|
87
|
+
property(context) {
|
|
88
|
+
return { pid: context.Identifier[0].image };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
attribute(context) {
|
|
92
|
+
return { aid: context.Identifier[0].image };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
entity(context) {
|
|
96
|
+
return { eid: context.Identifier[0].image };
|
|
97
|
+
}
|
|
98
|
+
|
|
82
99
|
}
|
|
83
100
|
|
|
84
101
|
const visitor = new StringToAstVisitor(); // this is stateless, so Singleton
|
package/src/str2fun.js
CHANGED
|
@@ -5,9 +5,10 @@ import { Parser } from './grammar.js';
|
|
|
5
5
|
const parser = new Parser(); // CST output is enabled by default
|
|
6
6
|
const CstVisitor = parser.getBaseCstVisitorConstructor();
|
|
7
7
|
|
|
8
|
-
function getMetadata(item, pid) { // TODO use heimdall.getMetadata(item, ...)
|
|
8
|
+
function getMetadata(item, aid, pid) { // TODO? use heimdall.getMetadata(item, ...)
|
|
9
9
|
for (const meta of item['metadata']) {
|
|
10
|
-
if (meta['
|
|
10
|
+
if (aid != null && meta['aid'] == aid) return meta['value'];
|
|
11
|
+
if (pid != null && meta['pid'] == pid) return meta['value'];
|
|
11
12
|
}
|
|
12
13
|
return null;
|
|
13
14
|
}
|
|
@@ -69,6 +70,9 @@ class StringToFunctionVisitor extends CstVisitor {
|
|
|
69
70
|
if (context.Not) {
|
|
70
71
|
return (item) => !this.visit(context.statement)(item);
|
|
71
72
|
} // else
|
|
73
|
+
if (context.entity) {
|
|
74
|
+
return this.visit(context.entity);
|
|
75
|
+
} // else
|
|
72
76
|
return this.visit(context.statement);
|
|
73
77
|
}
|
|
74
78
|
|
|
@@ -79,8 +83,9 @@ class StringToFunctionVisitor extends CstVisitor {
|
|
|
79
83
|
const right = this.visit(context.right[0]);
|
|
80
84
|
return function(item) {
|
|
81
85
|
if (typeof left === 'object') {
|
|
82
|
-
const
|
|
83
|
-
const
|
|
86
|
+
const aid = ('aid' in left ? left['aid'] : null);
|
|
87
|
+
const pid = ('pid' in left ? left['pid'] : null);
|
|
88
|
+
const value = getMetadata(item, aid, pid); // TODO what if null here ?
|
|
84
89
|
return filter(value, right);
|
|
85
90
|
}
|
|
86
91
|
return filter(left, right);
|
|
@@ -88,14 +93,14 @@ class StringToFunctionVisitor extends CstVisitor {
|
|
|
88
93
|
}
|
|
89
94
|
|
|
90
95
|
atom(context) {
|
|
91
|
-
|
|
96
|
+
if (context.attribute) return this.visit(context.attribute);
|
|
97
|
+
if (context.property) return this.visit(context.property);
|
|
92
98
|
if (context.Integer) return parseInt(context.Integer[0].image);
|
|
93
|
-
|
|
94
|
-
return { pid: context.Identifier[0].image };
|
|
99
|
+
return context.String[0].image;
|
|
95
100
|
}
|
|
96
101
|
|
|
97
102
|
relationalOperator(context) {
|
|
98
|
-
// note: these visitor methods will return a function
|
|
103
|
+
// note: these visitor methods will return a function returning a boolean
|
|
99
104
|
if (context.Greater) return (x, y) => x > y;
|
|
100
105
|
if (context.GreaterOrEqual) return (x, y) => x >= y;
|
|
101
106
|
if (context.Lesser) return (x, y) => x < y;
|
|
@@ -103,6 +108,22 @@ class StringToFunctionVisitor extends CstVisitor {
|
|
|
103
108
|
if (context.NotEqual) return (x, y) => x != y;
|
|
104
109
|
return (x, y) => x == y;
|
|
105
110
|
}
|
|
111
|
+
|
|
112
|
+
property(context) {
|
|
113
|
+
return { pid: context.Identifier[0].image };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
attribute(context) {
|
|
117
|
+
return { aid: context.Identifier[0].image };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
entity(context) {
|
|
121
|
+
return function(item) {
|
|
122
|
+
const eid = context.Identifier[0].image;
|
|
123
|
+
return item['eid'] == eid;
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
106
127
|
}
|
|
107
128
|
|
|
108
129
|
const visitor = new StringToFunctionVisitor(); // this is stateless, so Singleton
|
package/src/tokens.js
CHANGED
|
@@ -87,6 +87,21 @@ export const logic = {
|
|
|
87
87
|
export const P_LEFT = createToken({ name: 'PLeft', pattern: /\(/ });
|
|
88
88
|
export const P_RIGHT = createToken({ name: 'PRight', pattern: /\)/ });
|
|
89
89
|
|
|
90
|
+
// KEYWORDS
|
|
91
|
+
//////////////
|
|
92
|
+
export const PID = createToken({
|
|
93
|
+
name: 'PropertyID', pattern: /pid/,
|
|
94
|
+
longer_alt: Identifier,
|
|
95
|
+
});
|
|
96
|
+
export const EID = createToken({
|
|
97
|
+
name: 'EntityID', pattern: /eid/,
|
|
98
|
+
longer_alt: Identifier,
|
|
99
|
+
});
|
|
100
|
+
export const AID = createToken({
|
|
101
|
+
name: 'AttributeID', pattern: /aid/,
|
|
102
|
+
longer_alt: Identifier,
|
|
103
|
+
});
|
|
104
|
+
|
|
90
105
|
// WHITESPACE
|
|
91
106
|
////////////////
|
|
92
107
|
export const WhiteSpace = createToken({
|
|
@@ -97,7 +112,7 @@ export const WhiteSpace = createToken({
|
|
|
97
112
|
|
|
98
113
|
// /!\ order of tokens is important ! /!\
|
|
99
114
|
// for example, keywords could never be matched if declared after Identifier,
|
|
100
|
-
// or '!=' (NOT_EQUAL) could never be matched if declared after '!'
|
|
115
|
+
// or '!=' (NOT_EQUAL) could never be matched if declared after '!' (NOT)
|
|
101
116
|
export const TOKENS = [
|
|
102
117
|
WhiteSpace, // whitespace appears first to improve lexer performance
|
|
103
118
|
// >>> START relational operators
|
|
@@ -110,6 +125,11 @@ export const TOKENS = [
|
|
|
110
125
|
// <<< END relational operators
|
|
111
126
|
P_LEFT,
|
|
112
127
|
P_RIGHT,
|
|
128
|
+
// >>> START keywords
|
|
129
|
+
PID,
|
|
130
|
+
EID,
|
|
131
|
+
AID,
|
|
132
|
+
// <<< END keywords
|
|
113
133
|
// >>> START boolean operators
|
|
114
134
|
NOT,
|
|
115
135
|
AND,
|