type GroupedData = Map; 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(); 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); }); return groupedData; } private executeGroupBy(filteredData: any[] = this.data) { if (this.groupBySelectors.length === 0) { return new Map([['', 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; };