Skip to content

Commit 032567f

Browse files
committed
feat(query-builder): add SQL sample initial implementation
1 parent 3bd15b7 commit 032567f

File tree

9 files changed

+258
-5
lines changed

9 files changed

+258
-5
lines changed

live-editing/configs/QueryBuilderConfigGenerator.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { Config, IConfigGenerator} from 'igniteui-live-editing'
22
import { BaseAppConfig } from './BaseConfig';
33
export class QueryBuilderConfigGenerator implements IConfigGenerator {
4-
4+
public additionalImports = {
5+
RemoteLoDService: '../../src/app/services/remote-lod.service'
6+
};
57

68
public generateConfigs(): Config[] {
79
const configs = new Array<Config>();
@@ -25,6 +27,13 @@ export class QueryBuilderConfigGenerator implements IConfigGenerator {
2527
shortenComponentPathBy: "/interactions/query-builder/"
2628
}));
2729

30+
configs.push(new Config({
31+
component: 'QueryBuilderSqlSampleComponent',
32+
additionalFiles: ['/src/app/services/remote-lod.service.ts'],
33+
appConfig: BaseAppConfig,
34+
shortenComponentPathBy: "/interactions/query-builder/"
35+
}));
36+
2837
return configs;
2938
}
3039
}

package-lock.json

Lines changed: 24 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@
7676
"minireset.css": "0.0.6",
7777
"rxjs": "^7.8.1",
7878
"tslib": "^2.6.1",
79+
"xml2js": "^0.6.2",
7980
"zone.js": "~0.15.0"
8081
},
8182
"overrides": {

src/app/interactions/interactions-routes-data.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,5 +50,6 @@ export const interactionsRoutesData = {
5050
'icons-sample': { displayName: 'Icons sample', parentName: 'Drag and Drop' },
5151
'query-builder-sample-1': { displayName: 'Query Builder Sample 1', parentName: 'Query Builder' },
5252
'query-builder-style': { displayName: 'Query Builder Style Sample', parentName: 'Query Builder' },
53-
'query-builder-template-sample': { displayName: 'Query Builder Template Sample', parentName: 'Query Builder' }
53+
'query-builder-template-sample': { displayName: 'Query Builder Template Sample', parentName: 'Query Builder' },
54+
'query-builder-sql-sample': { displayName: 'Query Builder SQL Sample', parentName: 'Query Builder' }
5455
};

src/app/interactions/interactions.routes.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { OverlayStylingComponent } from './overlay/overlay-styling/overlay-styli
2323
import { QueryBuilderSample1Component } from './query-builder/query-builder-sample-1/query-builder-sample-1.component';
2424
import { QueryBuilderStyleComponent } from './query-builder/query-builder-style/query-builder-style.component';
2525
import { QueryBuilderTemplateSampleComponent } from './query-builder/query-builder-template-sample/query-builder-template-sample.component';
26+
import { QueryBuilderSqlSampleComponent } from './query-builder/query-builder-sql-sample/query-builder-sql-sample.component';
2627
import { RippleSample2Component } from './ripple/ripple-sample-2/ripple-sample-2.component';
2728
import { RippleSample3Component } from './ripple/ripple-sample-3/ripple-sample-3.component';
2829
import { RippleSample4Component } from './ripple/ripple-sample-4/ripple-sample-4.component';
@@ -304,5 +305,10 @@ export const InteractionsRoutes: Routes = [
304305
component: QueryBuilderTemplateSampleComponent,
305306
data: interactionsRoutesData['query-builder-template-sample'],
306307
path: 'query-builder-template-sample'
308+
},
309+
{
310+
component: QueryBuilderSqlSampleComponent,
311+
data: interactionsRoutesData['query-builder-sql-sample'],
312+
path: 'query-builder-sql-sample'
307313
}
308314
];
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<div class="wrapper">
2+
<igx-query-builder #queryBuilder
3+
[entities]="entities"
4+
[(expressionTree)]="expressionTree"
5+
(expressionTreeChange)="handleExpressionTreeChange($event)">
6+
</igx-query-builder>
7+
8+
<div class="output-area">
9+
<pre>{{sqlQuery}}</pre>
10+
</div>
11+
</div>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
.wrapper{
2+
margin: 10px;
3+
height: 100%;
4+
overflow-y: auto;
5+
}
6+
7+
.output-area{
8+
box-shadow: 0 0 1px 0 rgba(0, 0, 0, 0.75);
9+
border-radius: 4px;
10+
margin: 0 20px 20px 20px;
11+
word-break: break-all;
12+
word-wrap: break-word;
13+
}
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import { Component, OnInit, ViewChild } from '@angular/core';
2+
import { EntityType, FilteringExpressionsTree, IExpressionTree, IgxQueryBuilderComponent } from 'igniteui-angular';
3+
import { RemoteLoDService } from '../../../services/remote-lod.service';
4+
5+
@Component({
6+
providers: [RemoteLoDService],
7+
selector: 'app-query-builder-sql-sample',
8+
styleUrls: ['./query-builder-sql-sample.component.scss'],
9+
templateUrl: 'query-builder-sql-sample.component.html',
10+
imports: [IgxQueryBuilderComponent]
11+
})
12+
export class QueryBuilderSqlSampleComponent implements OnInit {
13+
@ViewChild('queryBuilder', { static: true })
14+
public queryBuilder: IgxQueryBuilderComponent;
15+
16+
public entities: EntityType[] = [];
17+
public expressionTree: IExpressionTree;
18+
public sqlQuery: string = 'Select an entity and build your query';
19+
20+
private dataTypeMap = {
21+
'Edm.String': 'string',
22+
'Edm.Int16': 'number',
23+
'Edm.Int32': 'number',
24+
'Edm.Int64': 'number',
25+
'Edm.Decimal': 'number',
26+
'Edm.Double': 'number',
27+
'Edm.Single': 'number',
28+
'Edm.Boolean': 'boolean',
29+
'Edm.DateTimeOffset': 'date',
30+
'Edm.Guid': 'string'
31+
};
32+
33+
constructor(private remoteService: RemoteLoDService) { }
34+
35+
public ngOnInit(): void {
36+
console.log('Query Builder SQL Sample');
37+
38+
const entities = [];
39+
this.remoteService.getMetadata().subscribe({
40+
next: (data) => {
41+
const schema = data['edmx:Edmx']['edmx:DataServices']['Schema'];
42+
const entityTypes = schema[0]['EntityType'];
43+
const entitySets = schema[1]['EntityContainer']['EntitySet'];
44+
entityTypes.forEach((entityType) => {
45+
const entityName = entityType['$'].Name;
46+
const fields = entityType['Property'].map((prop) => {
47+
return {
48+
field: prop['$'].Name,
49+
dataType: this.dataTypeMap[prop['$'].Type]
50+
}
51+
});
52+
const entityMatch = entitySets.find((entitySet) => entitySet['$'].EntityType === 'NorthwindModel.' + entityName);
53+
54+
const entity = {
55+
name: entityMatch['$'].Name,
56+
fields: fields
57+
}
58+
entities.push(entity);
59+
});
60+
},
61+
error: err => {
62+
console.log(err);
63+
},
64+
complete: () => {
65+
this.entities = entities;
66+
}
67+
});
68+
}
69+
70+
public handleExpressionTreeChange(event: IExpressionTree) {
71+
this.sqlQuery = this.transformExpressionTreeToSQL(this.queryBuilder.expressionTree);
72+
}
73+
74+
private transformExpressionTreeToSQL(tree: any): string {
75+
if (!tree) {
76+
return '';
77+
}
78+
79+
const selectClause = `SELECT \n ${tree.returnFields.length > 0 ? tree.returnFields.join('\n ') : '*'} `;
80+
const fromClause = `FROM ${tree.entity}`;
81+
const whereClause = this.buildWhereClause(tree);
82+
83+
return `\n${selectClause}\n${fromClause}${whereClause ? '\n' + whereClause : ''}\n`;
84+
}
85+
86+
private buildWhereClause(tree: IExpressionTree): string {
87+
if (!tree || !tree.filteringOperands || tree.filteringOperands.length === 0) {
88+
return '';
89+
}
90+
91+
let conditions = tree.filteringOperands.map(operand => {
92+
if (operand instanceof FilteringExpressionsTree) {
93+
return `(${this.buildWhereClause(operand)})`;
94+
} else {
95+
return this.buildCondition(operand);
96+
}
97+
});
98+
99+
const operator = tree.operator === 0 ? 'AND' : 'OR'; // 0 for AND, 1 for OR
100+
conditions = conditions.filter(cond => cond !== '');
101+
return conditions.length > 0 ? `WHERE ${conditions.join(` ${operator} `)}` : '';
102+
}
103+
104+
private buildCondition(operand: any): string {
105+
const field = operand.fieldName;
106+
const value = operand.searchVal;
107+
const condition = operand.condition.name;
108+
109+
// TODO: add missing conditions
110+
switch (condition) {
111+
case 'equals':
112+
return `${field} = '${value}'`;
113+
case 'contains':
114+
return `${field} LIKE '%${value}%'`;
115+
case 'startsWith':
116+
return `${field} LIKE '${value}%'`;
117+
case 'endsWith':
118+
return `${field} LIKE '%${value}'`;
119+
case 'greaterThan':
120+
return `${field} > ${value}`;
121+
case 'lessThan':
122+
return `${field} < ${value}`;
123+
case 'greaterThanOrEqualTo':
124+
return `${field} >= ${value}`;
125+
case 'lessThanOrEqualTo':
126+
return `${field} <= ${value}`;
127+
case 'doesNotEqual':
128+
return `${field} <> ${value}`;
129+
case 'doesNotContain':
130+
return `${field} NOT LIKE '%${value}%'`;
131+
case 'doesNotStartWith':
132+
return `${field} NOT LIKE '${value}%'`;
133+
case 'doesNotEndWith':
134+
return `${field} NOT LIKE '%${value}'`;
135+
case 'null':
136+
return `${field} IS NULL`;
137+
case 'notNull':
138+
return `${field} IS NOT NULL`;
139+
case 'empty':
140+
return `${field} = ''`;
141+
case 'notEmpty':
142+
return `${field} <> ''`;
143+
case 'true':
144+
return `${field} = true`;
145+
case 'false':
146+
return `${field} = false`;
147+
case 'inQuery':
148+
return `${field} IN (${this.transformExpressionTreeToSQL(operand.searchTree)})`;
149+
case 'notInQuery':
150+
return `${field} NOT IN (${this.transformExpressionTreeToSQL(operand.searchTree)})`;
151+
case 'before':
152+
return `${field} < DATEFROMPARTS(${value.getFullYear()}, ${value.getMonth() + 1}, ${value.getDate()})`;
153+
case 'after':
154+
return `${field} > DATEFROMPARTS(${value.getFullYear()}, ${value.getMonth() + 1}, ${value.getDate()})`;
155+
case 'today':
156+
return `CONVERT(DATE, ${field}) = CONVERT(DATE, GETDATE())`;
157+
case 'yesterday':
158+
return `CONVERT(DATE, ${field}) = CONVERT(DATE, DATEADD(DAY, -1, GETDATE()))`;
159+
case 'thisMonth':
160+
return `YEAR(${field}) = YEAR(GETDATE()) AND MONTH(${field}) = MONTH(GETDATE())`;
161+
case 'lastMonth':
162+
return `${field} BETWEEN DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()) - 1, 0) AND DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()), 0)`;
163+
case 'nextMonth':
164+
return `${field} BETWEEN DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()), 0) AND DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()) + 1, 0)`;
165+
case 'thisYear':
166+
return `YEAR(${field}) = YEAR(GETDATE())`;
167+
case 'lastYear':
168+
return `YEAR(${field}) = YEAR(GETDATE()) - 1`;
169+
case 'nextYear':
170+
return `YEAR(${field}) = YEAR(GETDATE()) + 1`;
171+
default:
172+
return '';
173+
}
174+
}
175+
}

src/app/services/remote-lod.service.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { HttpClient } from '@angular/common/http';
22
import { Injectable } from '@angular/core';
33
import { Observable } from 'rxjs';
44
import { map } from 'rxjs/operators';
5+
import { parseString } from 'xml2js';
56

67
interface IDataResponse {
78
value: any[];
@@ -26,6 +27,21 @@ export class RemoteLoDService {
2627
);
2728
}
2829

30+
public getMetadata(): Observable<object> {
31+
return this.http.get(`${this.url}$metadata`, { responseType: 'text' }).pipe(
32+
map((response: string) => {
33+
let result;
34+
parseString(response, { explicitArray: false }, (err, parsedResult) => {
35+
if (err) {
36+
throw new Error('Error parsing XML');
37+
}
38+
result = parsedResult;
39+
});
40+
return result;
41+
})
42+
);
43+
}
44+
2945
public buildUrl(dataState: IDataState) {
3046
let qS = '';
3147
if (dataState) {

0 commit comments

Comments
 (0)