233 lines
6.3 KiB
TypeScript
233 lines
6.3 KiB
TypeScript
type GroupedData = Map<string, any[] | GroupedData>;
|
|
|
|
class Query {
|
|
private data: any[];
|
|
private selector: (obj: any) => any = (obj) => obj;
|
|
private filterGroups: ((obj: any) => boolean)[][] = [];
|
|
private groupBySelectors: ((obj: any) => any)[] = [];
|
|
private groupFilterGroups: ((obj: any) => boolean)[][] = [];
|
|
private orderFunction: (obj1: any, obj2: any) => number = (obj1, obj2) => 0;
|
|
|
|
constructor() {
|
|
this.data = [];
|
|
}
|
|
|
|
// If you repeat a SQL clause (except where() or having()), an exception will be thrown
|
|
private _selected = false;
|
|
private _fromed = false;
|
|
private _grouped = false;
|
|
private _ordered = false;
|
|
|
|
select(newSelector?: (obj: any) => any) {
|
|
if (this._selected) {
|
|
throw new Error('Duplicate SELECT');
|
|
}
|
|
|
|
if (newSelector) {
|
|
this.selector = newSelector;
|
|
};
|
|
|
|
this._selected = true;
|
|
return this;
|
|
}
|
|
|
|
from(...dataRows: any[][]) {
|
|
if (this._fromed) {
|
|
throw new Error('Duplicate FROM');
|
|
}
|
|
|
|
this._fromed = true;
|
|
|
|
// cartesian product of all data arrays
|
|
dataRows.forEach((dataRow) => {
|
|
if (this.data.length === 0) {
|
|
this.data = dataRow;
|
|
return;
|
|
}
|
|
|
|
const newData: any[] = [];
|
|
this.data.forEach((obj) => {
|
|
dataRow.forEach((newObj) => {
|
|
newData.push([obj, newObj]);
|
|
});
|
|
})
|
|
|
|
this.data = newData;
|
|
})
|
|
|
|
return this;
|
|
}
|
|
|
|
where(...newFilters: ((obj: any) => boolean)[]) {
|
|
this.filterGroups.push(newFilters);
|
|
return this;
|
|
}
|
|
|
|
groupBy(...newGroupBySelectors: ((obj: any) => any)[]) {
|
|
if (this._grouped) {
|
|
throw new Error('Duplicate GROUPBY');
|
|
}
|
|
|
|
this._grouped = true;
|
|
this.groupBySelectors = newGroupBySelectors;
|
|
return this;
|
|
}
|
|
|
|
having(...newGroupFilters: ((obj: any) => boolean)[]) {
|
|
this.groupFilterGroups.push(newGroupFilters);
|
|
return this;
|
|
}
|
|
|
|
orderBy(newOrderFunction: (obj1: any, obj2: any) => number) {
|
|
if (this._ordered) {
|
|
throw new Error('Duplicate ORDERBY');
|
|
}
|
|
|
|
this._ordered = true;
|
|
this.orderFunction = newOrderFunction;
|
|
return this;
|
|
}
|
|
|
|
execute() {
|
|
// filter
|
|
this.data = this.executeFilter();
|
|
|
|
// group by
|
|
const groupedMap = this.executeGroupBy();
|
|
|
|
// filter groups
|
|
this.executeHaving(groupedMap);
|
|
|
|
const selected = this.executeSelect(groupedMap);
|
|
|
|
const result = this.executeOrderBy(selected);
|
|
return result;
|
|
}
|
|
|
|
private executeFilter() {
|
|
// this.filterGroups is a list of groups of filters
|
|
// filters in the same group are ORed
|
|
// groups are ANDed
|
|
let filteredData = this.data;
|
|
this.filterGroups.forEach((filterGroup) => {
|
|
filteredData = filteredData.filter((obj) => {
|
|
return filterGroup.some((filter) => filter(obj));
|
|
});
|
|
})
|
|
return filteredData;
|
|
}
|
|
|
|
private groupOnce(data: any[], groupBySelector: (obj: any) => any) {
|
|
const groupedData = new Map<string, any[] | GroupedData>();
|
|
data.forEach((obj) => {
|
|
const key = groupBySelector(obj);
|
|
if (!groupedData.has(key)) {
|
|
groupedData.set(key, []);
|
|
}
|
|
(groupedData.get(key) as any[]).push(obj);
|
|
});
|
|
|
|
return groupedData;
|
|
}
|
|
|
|
private groupRecursively(data: any[], groupBySelectors: ((obj: any) => any)[]) {
|
|
|
|
const [groupBySelector, ...restGroupBySelectors] = groupBySelectors;
|
|
const groupedData = this.groupOnce(data, groupBySelector);
|
|
if (restGroupBySelectors.length === 0) {
|
|
return groupedData;
|
|
}
|
|
|
|
Array.from(groupedData.keys()).forEach((key) => {
|
|
const group = groupedData.get(key) as any[];
|
|
groupedData.set(key, this.groupRecursively(group, restGroupBySelectors) as Map<string, any[]>);
|
|
});
|
|
|
|
return groupedData;
|
|
}
|
|
|
|
private executeGroupBy(filteredData: any[] = this.data) {
|
|
if (this.groupBySelectors.length === 0) {
|
|
return new Map<string, any[]>([['', filteredData]]);
|
|
}
|
|
|
|
const groupedData = this.groupRecursively(filteredData, this.groupBySelectors);
|
|
return groupedData;
|
|
}
|
|
|
|
private executeHaving(groupedData: GroupedData) {
|
|
if (this.groupFilterGroups.length === 0) return groupedData;
|
|
// filter groups
|
|
// groupFilterGroups is a list of groups of filters
|
|
// filters in the same group are ORed
|
|
// groups are ANDed
|
|
|
|
// first flatten the map to arrays
|
|
let flattened: { flatGroup: any[], key: string }[] = [];
|
|
Array.from(groupedData.keys()).forEach((key) => {
|
|
const group = groupedData.get(key) as GroupedData | any[];
|
|
const flattenedGroup = Array.isArray(group) ?
|
|
group
|
|
: this.flattenMapToArrays(group as GroupedData, this.selector);
|
|
flattened.push({ flatGroup: [key, flattenedGroup], key });
|
|
});
|
|
|
|
this.groupFilterGroups.forEach((groupFilters) => {
|
|
flattened = flattened.filter((group) => {
|
|
const toKeep = groupFilters.some((filter) => filter(group.flatGroup));
|
|
if (!toKeep) groupedData.delete(group.key);
|
|
return toKeep;
|
|
})
|
|
});
|
|
|
|
return groupedData;
|
|
}
|
|
|
|
private flattenMapToArrays(groupedData: GroupedData, selector?: (obj: any) => any) {
|
|
// flatten maps to arrays
|
|
const result: any[] = [];
|
|
Array.from(groupedData.keys()).forEach((key) => {
|
|
const group = groupedData.get(key) as GroupedData | any[];
|
|
const flattened = Array.isArray(group) ?
|
|
selector ? (group as any[]).map(selector)
|
|
: group as any[]
|
|
: this.flattenMapToArrays(group as GroupedData, selector)
|
|
result.push([key, flattened]);
|
|
});
|
|
|
|
return result;
|
|
}
|
|
|
|
private executeSelect(groupedData: GroupedData) {
|
|
const keys = Array.from(groupedData.keys());
|
|
if (keys.length === 1 && keys[0] === '') {
|
|
return (groupedData.get('') as any[])?.map(this.selector);
|
|
}
|
|
|
|
return Array.from(keys).map((key) => {
|
|
const group = groupedData.get(key) as GroupedData | any[];
|
|
if (this.groupBySelectors.length === 0) {
|
|
// no group by was done,
|
|
// selector selects from objects
|
|
return [key, (group as any[]).map(this.selector)];
|
|
}
|
|
|
|
const flattenedGroup = Array.isArray(group) ?
|
|
group
|
|
: this.flattenMapToArrays(group as GroupedData, this.selector);
|
|
// selector selects from groups
|
|
return this.selector([key, flattenedGroup]);
|
|
});
|
|
}
|
|
|
|
private executeOrderBy(selected: any[]) {
|
|
return selected.sort(this.orderFunction);
|
|
}
|
|
}
|
|
|
|
|
|
export function query() {
|
|
const q = new Query();
|
|
return q;
|
|
};
|