@mst003/milql 1.1.1

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.
@@ -0,0 +1,37 @@
1
+ <!-- Use this file to provide workspace-specific custom instructions to Copilot. For more details, visit https://code.visualstudio.com/docs/copilot/copilot-customization#_use-a-githubcopilotinstructionsmd-file -->
2
+ - [x] Verify that the copilot-instructions.md file in the .github directory is created. - File created successfully.
3
+
4
+ - [x] Clarify Project Requirements - Node.js npm package project for refactoring a plugin specified.
5
+
6
+ - [x] Scaffold the Project
7
+ <!--
8
+ Ensure that the previous step has been marked as completed.
9
+ Call project setup tool with projectType parameter.
10
+ Run scaffolding command to create project files and folders.
11
+ Use '.' as the working directory.
12
+ If no appropriate projectType is available, search documentation using available tools.
13
+ Otherwise, create the project structure manually using available file creation tools.
14
+ -->
15
+ - Created package.json, index.js, README.md, .gitignore manually.
16
+
17
+ - [x] Customize the Project
18
+ - Plugin logic ported from Java/Groovy to Node.js, ANTLR grammars adapted for JS, classes implemented in lib/
19
+
20
+ - [x] Install Required Extensions
21
+ - No extensions needed.
22
+
23
+ - [x] Compile the Project
24
+ - No compilation required for Node.js project.
25
+
26
+ - [x] Create and Run Task
27
+ - No tasks required.
28
+
29
+ - [x] Launch the Project
30
+ - Not applicable for npm package.
31
+
32
+ - [x] Ensure Documentation is Complete
33
+ - README.md created, HTML comments removed from copilot-instructions.md.
34
+
35
+ - Work through each checklist item systematically.
36
+ - Keep communication concise and focused.
37
+ - Follow development best practices.
package/.gitlab-ci.yml ADDED
@@ -0,0 +1,29 @@
1
+ stages:
2
+ #- test
3
+ - publish
4
+
5
+ variables:
6
+ NPM_REGISTRY: "https://gitlab.com/api/v4/projects/$CI_PROJECT_ID/packages/npm/"
7
+
8
+ cache:
9
+ paths:
10
+ - node_modules/
11
+
12
+ # test:
13
+ # image: node:20
14
+ # stage: test
15
+ # script:
16
+ # - npm ci
17
+ # - npm test
18
+
19
+ publish:
20
+ image: node:20
21
+ stage: publish
22
+ script:
23
+ - echo "Publishing package to GitLab NPM registry..."
24
+ - npm config set //gitlab.com/api/v4/projects/$CI_PROJECT_ID/packages/npm/:_authToken $GROUP_REGISTRY_TOKEN
25
+ - npm publish --registry "$NPM_REGISTRY"
26
+ rules:
27
+ - if: '$CI_COMMIT_TAG'
28
+ when: always
29
+ - when: never
package/README.md ADDED
@@ -0,0 +1,37 @@
1
+ # mil-ql
2
+
3
+ An implementation of the MIL Query Language for Node.js.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install mil-ql
9
+ ```
10
+
11
+ ## Setup
12
+
13
+ This package includes a built-in MILQL parser, so no ANTLR file generation is required.
14
+
15
+ ## Usage
16
+
17
+ ```javascript
18
+ const { Parser, HQLTransform } = require('mil-ql');
19
+
20
+ const hql = new HQLTransform('Entity');
21
+ Parser.parse('filters=name::eq::"Alice"&sort=-createdAt&limit=10', hql);
22
+
23
+ console.log(hql.query);
24
+ console.log(hql.parameters);
25
+ ```
26
+
27
+ ## API
28
+
29
+ - `HQLTransform(entityName)`: Creates an HQL transformer for the given entity.
30
+ - `BaseASTListener`: Base class for implementing AST listeners.
31
+ - `ASTListener`: Interface for AST listeners.
32
+ - `Operator`: Enum for filter operators.
33
+ - `SortDirection`: Enum for sort directions.
34
+
35
+ ## License
36
+
37
+ ISC
package/index.js ADDED
@@ -0,0 +1,18 @@
1
+ // Main entry point for the mil-ql npm package
2
+ // MIL Query Language implementation for Node.js
3
+
4
+ const HQLTransform = require('./lib/HQLTransform');
5
+ const BaseASTListener = require('./lib/BaseASTListener');
6
+ const ASTListener = require('./lib/ASTListener');
7
+ const Operator = require('./lib/Operator');
8
+ const SortDirection = require('./lib/SortDirection');
9
+ const Parser = require('./lib/Parser');
10
+
11
+ module.exports = {
12
+ HQLTransform,
13
+ BaseASTListener,
14
+ ASTListener,
15
+ Operator,
16
+ SortDirection,
17
+ Parser
18
+ };
@@ -0,0 +1,51 @@
1
+ class ASTListener {
2
+ enterLimit() {}
3
+ exitLimit(value) {}
4
+
5
+ enterOffset() {}
6
+ exitOffset(value) {}
7
+
8
+ enterSort() {}
9
+ exitSort() {}
10
+ enterSortTerm() {}
11
+ exitSortTerm(sortDirection, sortKey) {}
12
+
13
+ enterFullTextSearch() {}
14
+ exitFullTextSearch(searchString) {}
15
+
16
+ enterEmbedPath() {}
17
+ exitEmbedPath() {}
18
+ enterEmbedDepth() {}
19
+ exitEmbedDepth() {}
20
+ enterEmbedPathExpression() {}
21
+ exitEmbedPathExpression(path) {}
22
+
23
+ enterDepthExpression() {}
24
+ exitDepthExpression(value) {}
25
+
26
+ enterFilter() {}
27
+ exitFilter() {}
28
+ enterAndPredicate() {}
29
+ exitAndPredicate() {}
30
+ enterOrPredicate() {}
31
+ exitOrPredicate() {}
32
+ enterPredicateTerm() {}
33
+ exitPredicateTerm(operator, lhs) {}
34
+ enterPredicateRightIntegerOperand() {}
35
+ exitPredicateRightIntegerOperand(value) {}
36
+ enterPredicateRightDecimalOperand() {}
37
+ exitPredicateRightDecimalOperand(value) {}
38
+ enterPredicateRightDateOperand() {}
39
+ exitPredicateRightDateOperand(value) {}
40
+ enterPredicateRightBlankOperand() {}
41
+ exitPredicateRightBlankOperand() {}
42
+ enterPredicateRightStringOperand() {}
43
+ exitPredicateRightStringOperand(value) {}
44
+
45
+ enterField() {}
46
+ exitField() {}
47
+ enterFieldTerm() {}
48
+ exitFieldTerm(fieldKey) {}
49
+ }
50
+
51
+ module.exports = ASTListener;
@@ -0,0 +1,7 @@
1
+ const ASTListener = require('./ASTListener');
2
+
3
+ class BaseASTListener extends ASTListener {
4
+ // All methods are empty as in the base
5
+ }
6
+
7
+ module.exports = BaseASTListener;
@@ -0,0 +1,206 @@
1
+ const BaseASTListener = require('./BaseASTListener');
2
+ const SortDirection = require('./SortDirection');
3
+ const Operator = require('./Operator');
4
+
5
+ class HQLTransform extends BaseASTListener {
6
+ static ENTITY_ALIAS = 'e';
7
+
8
+ constructor(entity) {
9
+ super();
10
+ this.limit = 25;
11
+ this.offset = 0;
12
+ this.selectClause = "";
13
+ this.joinClause = "";
14
+ this.orderByClause = "";
15
+ this.whereClause = "";
16
+ this.parameters = {};
17
+ this.entity = entity;
18
+
19
+ this.orderByTerms = [];
20
+ this.selectTerms = [];
21
+ this.joinTerms = [];
22
+ this.predicates = [];
23
+ this.operands = [];
24
+ this.parameterId = 1;
25
+ }
26
+
27
+ getRecordParser() {
28
+ // TODO: implement HQLRecordParser
29
+ // return new HQLRecordParser(this.selectTerms);
30
+ }
31
+
32
+ get query() {
33
+ return `SELECT * FROM ${this.entity} ${HQLTransform.ENTITY_ALIAS} ${this.joinClause} ${this.whereClause} ${this.orderByClause}`;
34
+ }
35
+
36
+ get countQuery() {
37
+ return `select count(*) from ${this.entity} ${HQLTransform.ENTITY_ALIAS} ${this.whereClause}`;
38
+ }
39
+
40
+ // Pagination parsing
41
+
42
+ exitLimit(value) {
43
+ this.limit = value;
44
+ }
45
+
46
+ exitOffset(value) {
47
+ this.offset = value;
48
+ }
49
+
50
+ // Ordering parsing
51
+
52
+ exitSort() {
53
+ this.orderByClause = `order by ${this.orderByTerms.join(', ')}`;
54
+ }
55
+
56
+ exitSortTerm(sortDirection, sortKey) {
57
+ this.orderByTerms.push(`${sortKey.join('.')} ${this.asHQLSortDirection(sortDirection)}`);
58
+ }
59
+
60
+ // Projection parsing
61
+
62
+ exitField() {
63
+ this.selectClause = `select ${this.selectTerms.map(term => `${HQLTransform.ENTITY_ALIAS}.${term.join('.')}`).join(', ')}`;
64
+ }
65
+
66
+ exitFieldTerm(fieldTerm) {
67
+ this.selectTerms.push(fieldTerm);
68
+ }
69
+
70
+ // Embed parsing
71
+
72
+ exitEmbedPath() {
73
+ if (this.joinTerms.length > 0) {
74
+ this.joinClause = this.joinTerms.map(term => `left join fetch ${HQLTransform.ENTITY_ALIAS}.${term.join('.')}`).join(' ');
75
+ }
76
+ }
77
+
78
+ exitEmbedPathExpression(path) {
79
+ this.joinTerms.push(path);
80
+ }
81
+
82
+ // Filtering parsing
83
+
84
+ exitFilter() {
85
+ this.whereClause = `where ${this.predicates.pop()}`;
86
+ }
87
+
88
+ exitAndPredicate() {
89
+ const rhs = this.predicates.pop();
90
+ const lhs = this.predicates.pop();
91
+ this.predicates.push(`(${lhs} and ${rhs})`);
92
+ }
93
+
94
+ exitOrPredicate() {
95
+ const rhs = this.predicates.pop();
96
+ const lhs = this.predicates.pop();
97
+ this.predicates.push(`(${lhs} or ${rhs})`);
98
+ }
99
+
100
+ exitPredicateTerm(operator, lhs) {
101
+ const rhs = this.operands.pop();
102
+ const paramName = rhs.slice(1);
103
+ this.applyOperatorParameter(operator, paramName);
104
+
105
+ const fieldExpression = this.asHQLExpression(operator, lhs.join('.'));
106
+ this.predicates.push(`(${fieldExpression} ${this.asHQLOperator(operator)} ${rhs})`);
107
+ }
108
+
109
+ exitPredicateRightIntegerOperand(value) {
110
+ this.addParameter(value);
111
+ }
112
+
113
+ exitPredicateRightDecimalOperand(value) {
114
+ this.addParameter(value);
115
+ }
116
+
117
+ exitPredicateRightDateOperand(value) {
118
+ this.addParameter(value);
119
+ }
120
+
121
+ exitPredicateRightBlankOperand() {
122
+ this.addParameter(null);
123
+ }
124
+
125
+ exitPredicateRightStringOperand(value) {
126
+ this.addParameter(value);
127
+ }
128
+
129
+ addParameter(value) {
130
+ const paramName = `param_${this.parameterId++}`;
131
+ this.parameters[paramName] = value;
132
+ this.operands.push(`:${paramName}`);
133
+ return paramName;
134
+ }
135
+
136
+ applyOperatorParameter(operator, paramName) {
137
+ let value = this.parameters[paramName];
138
+ if (value == null) {
139
+ return;
140
+ }
141
+
142
+ switch (operator) {
143
+ case Operator.CONTAINS_SUBSTRING:
144
+ case Operator.NOT_CONTAINS_SUBSTRING:
145
+ value = `%${value}%`;
146
+ break;
147
+ case Operator.STARTS_WITH:
148
+ case Operator.STARTS_WITH_LOWCASE_IGNOREACCENT:
149
+ value = `${value}%`;
150
+ break;
151
+ case Operator.ENDS_WITH:
152
+ value = `%${value}`;
153
+ break;
154
+ case Operator.EQUALS_WITH_LOWCASE_IGNOREACCENT:
155
+ value = `${value}`.toLowerCase();
156
+ break;
157
+ case Operator.STARTS_WITH_LOWCASE_IGNOREACCENT:
158
+ value = `${value}`.toLowerCase();
159
+ break;
160
+ default:
161
+ break;
162
+ }
163
+
164
+ this.parameters[paramName] = value;
165
+ }
166
+
167
+ asHQLExpression(operator, field) {
168
+ switch (operator) {
169
+ case Operator.EQUALS_WITH_LOWCASE_IGNOREACCENT:
170
+ case Operator.STARTS_WITH_LOWCASE_IGNOREACCENT:
171
+ return `lower(${field})`;
172
+ default:
173
+ return field;
174
+ }
175
+ }
176
+
177
+ asHQLSortDirection(sortDirection) {
178
+ switch (sortDirection) {
179
+ case SortDirection.DESC: return 'desc';
180
+ case SortDirection.ASC: return 'asc';
181
+ default: throw new Error("Invalid sort direction");
182
+ }
183
+ }
184
+
185
+ asHQLOperator(operator) {
186
+ switch (operator) {
187
+ case Operator.EQUALS: return '=';
188
+ case Operator.NOT_EQUALS: return '!=';
189
+ case Operator.GREATER_THAN: return '>';
190
+ case Operator.GREATER_OR_EQUAL_TO: return '>=';
191
+ case Operator.LESS_THAN: return '<';
192
+ case Operator.LESS_THAN_OR_EQUAL_TO: return '<=';
193
+ case Operator.CONTAINS_SUBSTRING: return 'like';
194
+ case Operator.NOT_CONTAINS_SUBSTRING: return 'not like';
195
+ case Operator.STARTS_WITH: return 'like';
196
+ case Operator.ENDS_WITH: return 'like';
197
+ case Operator.STARTS_WITH_LOWCASE_IGNOREACCENT: return 'like';
198
+ case Operator.EQUALS_WITH_LOWCASE_IGNOREACCENT: return '=';
199
+ case Operator.MATCHES_REGEX: return 'regexp';
200
+ case Operator.NOT_MATCHES_REGEX: return 'not regexp';
201
+ default: throw new Error('Unsupported operator.');
202
+ }
203
+ }
204
+ }
205
+
206
+ module.exports = HQLTransform;
@@ -0,0 +1,27 @@
1
+ const Operator = {
2
+ EQUALS: 'eq',
3
+ NOT_EQUALS: 'neq',
4
+ GREATER_THAN: 'gt',
5
+ GREATER_OR_EQUAL_TO: 'gte',
6
+ LESS_THAN: 'lt',
7
+ LESS_THAN_OR_EQUAL_TO: 'lte',
8
+ CONTAINS_SUBSTRING: 'ct',
9
+ NOT_CONTAINS_SUBSTRING: 'nct',
10
+ STARTS_WITH: 'sw',
11
+ ENDS_WITH: 'ew',
12
+ MATCHES_REGEX: 'mre',
13
+ NOT_MATCHES_REGEX: 'nmre',
14
+ STARTS_WITH_LOWCASE_IGNOREACCENT: 'swlcia',
15
+ EQUALS_WITH_LOWCASE_IGNOREACCENT: 'eqlcia'
16
+ };
17
+
18
+ Operator.from = function(test) {
19
+ for (let key in Operator) {
20
+ if (Operator[key] === test) {
21
+ return key;
22
+ }
23
+ }
24
+ return null;
25
+ };
26
+
27
+ module.exports = Operator;
package/lib/Parser.js ADDED
@@ -0,0 +1,278 @@
1
+ const Operator = require('./Operator');
2
+ const SortDirection = require('./SortDirection');
3
+
4
+ const OPERATOR_MAP = {
5
+ eq: Operator.EQUALS,
6
+ neq: Operator.NOT_EQUALS,
7
+ gt: Operator.GREATER_THAN,
8
+ gte: Operator.GREATER_OR_EQUAL_TO,
9
+ lt: Operator.LESS_THAN,
10
+ lte: Operator.LESS_THAN_OR_EQUAL_TO,
11
+ ct: Operator.CONTAINS_SUBSTRING,
12
+ nct: Operator.NOT_CONTAINS_SUBSTRING,
13
+ sw: Operator.STARTS_WITH,
14
+ ew: Operator.ENDS_WITH,
15
+ mre: Operator.MATCHES_REGEX,
16
+ nmre: Operator.NOT_MATCHES_REGEX,
17
+ swlcia: Operator.STARTS_WITH_LOWCASE_IGNOREACCENT,
18
+ eqlcia: Operator.EQUALS_WITH_LOWCASE_IGNOREACCENT
19
+ };
20
+
21
+ class Parser {
22
+ static parse(input, astListener) {
23
+ const query = input.startsWith('?') ? input.slice(1) : input;
24
+ const params = new URLSearchParams(query);
25
+
26
+ for (const [name, value] of params) {
27
+ switch (name) {
28
+ case 'limit':
29
+ astListener.enterLimit();
30
+ astListener.exitLimit(parseInt(value, 10));
31
+ break;
32
+ case 'offset':
33
+ astListener.enterOffset();
34
+ astListener.exitOffset(parseInt(value, 10));
35
+ break;
36
+ case 'sort':
37
+ Parser.parseSort(value, astListener);
38
+ break;
39
+ case 'fields':
40
+ Parser.parseFields(value, astListener);
41
+ break;
42
+ case 'embed':
43
+ Parser.parseEmbed(value, astListener);
44
+ break;
45
+ case 'filters':
46
+ Parser.parseFilters(value, astListener);
47
+ break;
48
+ case 'q':
49
+ astListener.enterFullTextSearch();
50
+ astListener.exitFullTextSearch(value);
51
+ break;
52
+ default:
53
+ break;
54
+ }
55
+ }
56
+
57
+ return astListener;
58
+ }
59
+
60
+ static parseSort(value, astListener) {
61
+ astListener.enterSort();
62
+ const terms = value.split(',').filter(Boolean);
63
+ for (const term of terms) {
64
+ astListener.enterSortTerm();
65
+ const direction = term.startsWith('-') ? SortDirection.DESC : SortDirection.ASC;
66
+ const path = term.replace(/^-/, '').split('.').filter(Boolean);
67
+ astListener.exitSortTerm(direction, path);
68
+ }
69
+ astListener.exitSort();
70
+ }
71
+
72
+ static parseFields(value, astListener) {
73
+ astListener.enterField();
74
+ const fields = value.split(',').filter(Boolean);
75
+ for (const field of fields) {
76
+ const path = field.split('.').filter(Boolean);
77
+ astListener.enterFieldTerm();
78
+ astListener.exitFieldTerm(path);
79
+ }
80
+ astListener.exitField();
81
+ }
82
+
83
+ static parseEmbed(value, astListener) {
84
+ astListener.enterEmbedPath();
85
+ const paths = value.split(',').filter(Boolean);
86
+ for (const pathText of paths) {
87
+ const path = pathText.split('.').filter(Boolean);
88
+ astListener.enterEmbedPathExpression();
89
+ astListener.exitEmbedPathExpression(path);
90
+ }
91
+ astListener.exitEmbedPath();
92
+ }
93
+
94
+ static parseFilters(value, astListener) {
95
+ astListener.enterFilter();
96
+ const tokens = Parser.tokenizeFilter(value);
97
+ const ast = Parser.parseFilterExpression(tokens);
98
+ Parser.walkFilter(ast, astListener);
99
+ astListener.exitFilter();
100
+ }
101
+
102
+ static tokenizeFilter(value) {
103
+ const tokens = [];
104
+ let text = value.trim();
105
+
106
+ while (text.length > 0) {
107
+ if (text.startsWith('(')) {
108
+ tokens.push({ type: 'lparen' });
109
+ text = text.slice(1).trimStart();
110
+ continue;
111
+ }
112
+ if (text.startsWith(')')) {
113
+ tokens.push({ type: 'rparen' });
114
+ text = text.slice(1).trimStart();
115
+ continue;
116
+ }
117
+ if (text.startsWith(';')) {
118
+ tokens.push({ type: 'and' });
119
+ text = text.slice(1).trimStart();
120
+ continue;
121
+ }
122
+ if (text.startsWith(',')) {
123
+ tokens.push({ type: 'or' });
124
+ text = text.slice(1).trimStart();
125
+ continue;
126
+ }
127
+
128
+ const operatorMatch = text.match(/^([A-Za-z0-9_]+(?:\.[A-Za-z0-9_]+)*)::(eq|neq|gt|gte|lt|lte|ct|nct|sw|ew|mre|nmre|eqlcia|swlcia)::/);
129
+ if (operatorMatch) {
130
+ tokens.push({ type: 'term', lhs: operatorMatch[1].split('.'), operator: operatorMatch[2] });
131
+ text = text.slice(operatorMatch[0].length).trimStart();
132
+ continue;
133
+ }
134
+
135
+ const blankMatch = text.match(/^BLANK\b/);
136
+ if (blankMatch) {
137
+ tokens.push({ type: 'blank' });
138
+ text = text.slice(blankMatch[0].length).trimStart();
139
+ continue;
140
+ }
141
+
142
+ const quotedMatch = text.match(/^"([^"\\]*(?:\\.[^"\\]*)*)"/);
143
+ if (quotedMatch) {
144
+ tokens.push({ type: 'string', value: quotedMatch[1].replace(/\\"/g, '"') });
145
+ text = text.slice(quotedMatch[0].length).trimStart();
146
+ continue;
147
+ }
148
+
149
+ const numberMatch = text.match(/^([0-9]+(?:\.[0-9]+)?)/);
150
+ if (numberMatch) {
151
+ tokens.push({ type: 'number', value: numberMatch[1] });
152
+ text = text.slice(numberMatch[0].length).trimStart();
153
+ continue;
154
+ }
155
+
156
+ throw new Error(`Unable to parse filters near: ${text}`);
157
+ }
158
+
159
+ return tokens;
160
+ }
161
+
162
+ static parseFilterExpression(tokens, minPrec = 0) {
163
+ let node = Parser.parseFilterPrimary(tokens);
164
+
165
+ while (tokens.length > 0 && Parser.getPrecedence(tokens[0]) >= minPrec) {
166
+ const opToken = tokens.shift();
167
+ const precedence = Parser.getPrecedence(opToken);
168
+ const nextMinPrec = precedence + 1;
169
+ const rhs = Parser.parseFilterExpression(tokens, nextMinPrec);
170
+ node = { type: opToken.type, left: node, right: rhs };
171
+ }
172
+
173
+ return node;
174
+ }
175
+
176
+ static parseFilterPrimary(tokens) {
177
+ const token = tokens.shift();
178
+ if (!token) {
179
+ throw new Error('Unexpected end of filter expression');
180
+ }
181
+
182
+ if (token.type === 'lparen') {
183
+ const node = Parser.parseFilterExpression(tokens);
184
+ const next = tokens.shift();
185
+ if (!next || next.type !== 'rparen') {
186
+ throw new Error('Missing closing parenthesis in filter expression');
187
+ }
188
+ return node;
189
+ }
190
+
191
+ if (token.type === 'term') {
192
+ const rhs = Parser.parseFilterValue(tokens);
193
+ return {
194
+ type: 'term',
195
+ lhs: token.lhs,
196
+ operator: token.operator,
197
+ rhs
198
+ };
199
+ }
200
+
201
+ throw new Error(`Unexpected token in filter expression: ${JSON.stringify(token)}`);
202
+ }
203
+
204
+ static parseFilterValue(tokens) {
205
+ const token = tokens.shift();
206
+ if (!token) {
207
+ throw new Error('Filter value expected');
208
+ }
209
+
210
+ if (token.type === 'blank') {
211
+ return { type: 'blank', value: null };
212
+ }
213
+ if (token.type === 'string') {
214
+ const dateValue = /^\d{4}-\d{2}-\d{2}$/.test(token.value) ? new Date(token.value) : token.value;
215
+ return { type: dateValue instanceof Date ? 'date' : 'string', value: dateValue };
216
+ }
217
+ if (token.type === 'number') {
218
+ return token.value.includes('.')
219
+ ? { type: 'decimal', value: parseFloat(token.value) }
220
+ : { type: 'integer', value: parseInt(token.value, 10) };
221
+ }
222
+
223
+ throw new Error(`Invalid filter value token: ${JSON.stringify(token)}`);
224
+ }
225
+
226
+ static getPrecedence(token) {
227
+ if (!token) return -1;
228
+ if (token.type === 'and') return 2;
229
+ if (token.type === 'or') return 1;
230
+ return -1;
231
+ }
232
+
233
+ static walkFilter(node, astListener) {
234
+ if (!node) return;
235
+
236
+ if (node.type === 'and') {
237
+ astListener.enterAndPredicate();
238
+ Parser.walkFilter(node.left, astListener);
239
+ Parser.walkFilter(node.right, astListener);
240
+ astListener.exitAndPredicate();
241
+ return;
242
+ }
243
+
244
+ if (node.type === 'or') {
245
+ astListener.enterOrPredicate();
246
+ Parser.walkFilter(node.left, astListener);
247
+ Parser.walkFilter(node.right, astListener);
248
+ astListener.exitOrPredicate();
249
+ return;
250
+ }
251
+
252
+ astListener.enterPredicateTerm();
253
+
254
+ if (node.rhs.type === 'integer') {
255
+ astListener.enterPredicateRightIntegerOperand();
256
+ astListener.exitPredicateRightIntegerOperand(node.rhs.value);
257
+ } else if (node.rhs.type === 'decimal') {
258
+ astListener.enterPredicateRightDecimalOperand();
259
+ astListener.exitPredicateRightDecimalOperand(node.rhs.value);
260
+ } else if (node.rhs.type === 'date') {
261
+ astListener.enterPredicateRightDateOperand();
262
+ astListener.exitPredicateRightDateOperand(node.rhs.value);
263
+ } else if (node.rhs.type === 'blank') {
264
+ astListener.enterPredicateRightBlankOperand();
265
+ astListener.exitPredicateRightBlankOperand();
266
+ } else if (node.rhs.type === 'string') {
267
+ astListener.enterPredicateRightStringOperand();
268
+ astListener.exitPredicateRightStringOperand(node.rhs.value);
269
+ } else {
270
+ throw new Error(`Unsupported filter rhs type: ${node.rhs.type}`);
271
+ }
272
+
273
+ astListener.exitPredicateTerm(OPERATOR_MAP[node.operator], node.lhs);
274
+ }
275
+ }
276
+
277
+
278
+ module.exports = Parser;
@@ -0,0 +1,6 @@
1
+ const SortDirection = {
2
+ DESC: 'DESC',
3
+ ASC: 'ASC'
4
+ };
5
+
6
+ module.exports = SortDirection;
@@ -0,0 +1,97 @@
1
+ const antlr4 = require('antlr4');
2
+
3
+ class CSTListener extends require('../MILQLParserListener') {
4
+ constructor(astListener) {
5
+ super();
6
+ this.astListener = astListener;
7
+ }
8
+
9
+ // Limit
10
+ enterLimitParameter(ctx) {
11
+ this.astListener.enterLimit();
12
+ }
13
+ exitLimitParameter(ctx) {
14
+ this.astListener.exitLimit(this.asInteger(ctx.Integer));
15
+ }
16
+
17
+ // Offset
18
+ enterOffsetParameter(ctx) {
19
+ this.astListener.enterOffset();
20
+ }
21
+ exitOffsetParameter(ctx) {
22
+ this.astListener.exitOffset(this.asInteger(ctx.Integer));
23
+ }
24
+
25
+ // Sort
26
+ enterSortParameter(ctx) {
27
+ this.astListener.enterSort();
28
+ }
29
+ exitSortParameter(ctx) {
30
+ this.astListener.exitSort();
31
+ }
32
+ enterSortExpression(ctx) {
33
+ this.astListener.enterSortTerm();
34
+ }
35
+ sortIdentifierPath = [];
36
+ enterSortIdentifierPath(ctx) {
37
+ this.sortIdentifierPath = [];
38
+ }
39
+ exitSortIdentifierStep(ctx) {
40
+ this.sortIdentifierPath.push(this.decode(ctx.SortIdentifier.getText()));
41
+ }
42
+ exitSortExpression(ctx) {
43
+ const direction = ctx.ReverseSort ? require('../SortDirection').DESC : require('../SortDirection').ASC;
44
+ this.astListener.exitSortTerm(direction, [...this.sortIdentifierPath]);
45
+ }
46
+
47
+ // Field
48
+ enterFieldParameter(ctx) {
49
+ this.astListener.enterField();
50
+ }
51
+ exitFieldParameter(ctx) {
52
+ this.astListener.exitField();
53
+ }
54
+ enterFieldExpression(ctx) {
55
+ this.astListener.enterFieldTerm();
56
+ }
57
+ fieldIdentifierPath = [];
58
+ enterFieldIdentifierPath(ctx) {
59
+ this.fieldIdentifierPath = [];
60
+ }
61
+ exitFieldIdentifierStep(ctx) {
62
+ this.fieldIdentifierPath.push(this.decode(ctx.FieldIdentifier.getText()));
63
+ }
64
+ exitFieldExpression(ctx) {
65
+ this.astListener.exitFieldTerm([...this.fieldIdentifierPath]);
66
+ }
67
+
68
+ // Embed
69
+ exitEmbedPathExpression(ctx) {
70
+ // Collect path - simplified
71
+ const path = []; // TODO: implement path collection
72
+ this.astListener.exitEmbedPathExpression(path);
73
+ }
74
+
75
+ // Filter - TODO: implement complex filter parsing
76
+ exitFilter(ctx) {
77
+ this.astListener.exitFilter();
78
+ }
79
+
80
+ // Search
81
+ enterSearchParameter(ctx) {
82
+ this.astListener.enterFullTextSearch();
83
+ }
84
+ exitSearchParameter(ctx) {
85
+ this.astListener.exitFullTextSearch(this.decode(ctx.ParameterValue.getText()));
86
+ }
87
+
88
+ // Helpers
89
+ asInteger(token) {
90
+ return parseInt(token.getText());
91
+ }
92
+ decode(str) {
93
+ return decodeURIComponent(str);
94
+ }
95
+ }
96
+
97
+ module.exports = CSTListener;
package/package.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "@mst003/milql",
3
+ "version": "1.1.1",
4
+ "description": "An implementation of the MIL Query Language for Node.js",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "test": "node tempTest.js"
8
+ },
9
+ "keywords": ["milql", "query", "language", "hql"],
10
+ "author": "",
11
+ "license": "ISC",
12
+ "type": "commonjs"
13
+
14
+ }
@@ -0,0 +1,203 @@
1
+ lexer grammar MILQLLexer;
2
+
3
+ // @header {
4
+ // }
5
+
6
+ // ----------------------------------------------------------------------------
7
+ // mode default;
8
+
9
+ Field
10
+ : 'fields' Equals -> pushMode(FieldMode)
11
+ ;
12
+
13
+ Filter
14
+ : 'filters' Equals -> pushMode(FilterMode)
15
+ ;
16
+
17
+ Embed
18
+ : 'embed' Equals -> pushMode(EmbedMode)
19
+ ;
20
+
21
+ Limit
22
+ : 'limit' Equals
23
+ ;
24
+
25
+ Offset
26
+ : 'offset' Equals
27
+ ;
28
+
29
+ Search
30
+ : 'q' Equals
31
+ ;
32
+
33
+ Sort
34
+ : 'sort' Equals -> pushMode(SortMode)
35
+ ;
36
+
37
+ Equals
38
+ : '='
39
+ ;
40
+
41
+ Ampersand
42
+ : '&'
43
+ ;
44
+
45
+ Integer
46
+ : ( [0-9] )+ ( 'L' )?
47
+ ;
48
+
49
+ Decimal
50
+ : ( [0-9] )+ '.' ( [0-9] )+ ( 'D' )?
51
+ ;
52
+
53
+ Identifier
54
+ : ( [a-z] | [A-Z] | [0-9] | '_' )+
55
+ ;
56
+
57
+ ParameterValue
58
+ : ( [a-z] | [A-Z] | [0-9]| ',' | '-' | '.' | '%' | '+' )+
59
+ ;
60
+
61
+ // ----------------------------------------------------------------------------
62
+ mode EmbedMode;
63
+
64
+ Depth
65
+ : 'DEPTH.'
66
+ ;
67
+
68
+ PathExpressionSeparator
69
+ : '%2C' | ','
70
+ ;
71
+
72
+ PathStepSeparator
73
+ : '.'
74
+ ;
75
+
76
+ DepthValue
77
+ : Integer
78
+ ;
79
+
80
+ PathStepIdentifier
81
+ : Identifier
82
+ ;
83
+
84
+ EndEmbedMode
85
+ : '&' -> popMode
86
+ ;
87
+
88
+ // ----------------------------------------------------------------------------
89
+ mode SortMode;
90
+
91
+ ReverseSort
92
+ : '-'
93
+ ;
94
+
95
+ SortExpressionSeparator
96
+ : '%2C' | ','
97
+ ;
98
+
99
+ SortIdentifierStepSeparator
100
+ : '.'
101
+ ;
102
+
103
+ SortIdentifier
104
+ : Identifier
105
+ ;
106
+
107
+ EndSortMode
108
+ : '&' -> popMode
109
+ ;
110
+
111
+ // ----------------------------------------------------------------------------
112
+ mode FieldMode;
113
+
114
+ FieldExpressionSeparator
115
+ : '%2C' | ','
116
+ ;
117
+
118
+ FieldIdentifierStepSeparator
119
+ : '.'
120
+ ;
121
+
122
+ EndFieldMode
123
+ : '&' -> popMode
124
+ ;
125
+
126
+ FieldIdentifier
127
+ : Identifier
128
+ ;
129
+
130
+ // ----------------------------------------------------------------------------
131
+ mode FilterMode;
132
+
133
+ AndFilter
134
+ : '%3B' | ';'
135
+ ;
136
+
137
+ OrFilter
138
+ : '%2C' | ','
139
+ ;
140
+
141
+ FilterOperation
142
+ : ('%3A%3A' | '::')
143
+ ( 'eq'
144
+ | 'neq'
145
+ | 'gt'
146
+ | 'gte'
147
+ | 'lt'
148
+ | 'lte'
149
+ | 'ct'
150
+ | 'nct'
151
+ | 'sw'
152
+ | 'ew'
153
+ | 'mre'
154
+ | 'nmre'
155
+ | 'eqlcia'
156
+ | 'swlcia'
157
+ )
158
+ ('%3A%3A' | '::')
159
+ ;
160
+
161
+ FilterLeftParens
162
+ : '%28' | '('
163
+ ;
164
+
165
+ FilterRightParens
166
+ : '%29' | ')'
167
+ ;
168
+
169
+ FilterBlank
170
+ : 'BLANK'
171
+ ;
172
+
173
+ FilterInteger
174
+ : Integer
175
+ ;
176
+
177
+ FilterDecimal
178
+ : Decimal
179
+ ;
180
+
181
+ FilterIdentifierStepSeparator
182
+ : '.'
183
+ ;
184
+
185
+ FilterIdentifier
186
+ : Identifier
187
+ ;
188
+
189
+ Digit
190
+ : [0-9]
191
+ ;
192
+
193
+ HexDigit
194
+ : [0-9] | [A-F]
195
+ ;
196
+
197
+ FilterDate
198
+ : '%22' Digit Digit Digit Digit '-' Digit Digit '-' Digit Digit '%22'
199
+ ;
200
+
201
+ FilterString
202
+ : '"' ( ~'"' )* '"'
203
+ ;
@@ -0,0 +1,102 @@
1
+ parser grammar MILQLParser;
2
+
3
+ options { tokenVocab=MILQLLexer; }
4
+
5
+ // @header {
6
+ // }
7
+
8
+ parameterList
9
+ : ( parameter
10
+ ( ( Ampersand
11
+ | EndEmbedMode
12
+ | EndSortMode
13
+ | EndFilterMode
14
+ | EndFieldMode
15
+ )
16
+ parameter
17
+ )*
18
+ )?
19
+ EOF
20
+ ;
21
+
22
+ parameter
23
+ : Embed pathExpressions # EmbedPathParameter
24
+ | Embed depthExpression # EmbedDepthParameter
25
+ | Field fieldExpressions # FieldParameter
26
+ | Filter filterExpression # FilterParameter
27
+ | Limit Integer # LimitParameter
28
+ | Offset Integer # OffsetParameter
29
+ | Search ParameterValue # SearchParameter
30
+ | Sort sortExpressions # SortParameter
31
+ ;
32
+
33
+ pathExpressions
34
+ : pathExpression ( PathExpressionSeparator pathExpression )*
35
+ ;
36
+
37
+ pathExpression
38
+ : pathStepExpression ( PathStepSeparator pathStepExpression )*
39
+ ;
40
+
41
+ pathStepExpression
42
+ : PathStepIdentifier
43
+ ;
44
+
45
+ depthExpression
46
+ : Depth DepthValue
47
+ ;
48
+
49
+ fieldExpressions
50
+ : fieldExpression ( FieldExpressionSeparator fieldExpression )*
51
+ ;
52
+
53
+ fieldExpression
54
+ : fieldIdentifierPath
55
+ ;
56
+
57
+ fieldIdentifierPath
58
+ : fieldIdentifierStep ( FieldIdentifierStepSeparator fieldIdentifierStep )*
59
+ ;
60
+
61
+ fieldIdentifierStep
62
+ : FieldIdentifier
63
+ ;
64
+
65
+ filterExpression
66
+ : filterExpression AndFilter filterExpression # AndFilterExpression
67
+ | filterExpression OrFilter filterExpression # OrFilterExpression
68
+ | filterIdentifierPath FilterOperation filterTestValue # FilterTermExpression
69
+ | FilterLeftParens filterExpression FilterRightParens # FilterGroup
70
+ ;
71
+
72
+ filterIdentifierPath
73
+ : filterIdentifierStep ( FilterIdentifierStepSeparator filterIdentifierStep )*
74
+ ;
75
+
76
+ filterIdentifierStep
77
+ : FilterIdentifier
78
+ ;
79
+
80
+ filterTestValue
81
+ : FilterInteger # FilterInteger
82
+ | FilterDecimal # FilterDecimal
83
+ | FilterDate # FilterDate
84
+ | FilterBlank # FilterBlank
85
+ | FilterString # FilterString
86
+ ;
87
+
88
+ sortExpressions
89
+ : sortExpression ( SortExpressionSeparator sortExpression )*
90
+ ;
91
+
92
+ sortExpression
93
+ : ReverseSort? sortIdentifierPath
94
+ ;
95
+
96
+ sortIdentifierPath
97
+ : sortIdentifierStep ( SortIdentifierStepSeparator sortIdentifierStep )*
98
+ ;
99
+
100
+ sortIdentifierStep
101
+ : SortIdentifier
102
+ ;
package/tempTest.js ADDED
@@ -0,0 +1,6 @@
1
+ const { Parser, HQLTransform } = require('./index');
2
+ const hql = new HQLTransform('User');
3
+ Parser.parse('filters=name::eq::"Alice"&sort=-createdAt&limit=10&offset=5&q=hello', hql);
4
+ console.log(hql.query);
5
+ console.log(JSON.stringify(hql.parameters));
6
+ console.log(hql.limit, hql.offset);