From 47acb503cb7c0cbc3238d89e4a9a80eb7aedc765 Mon Sep 17 00:00:00 2001 From: Siddhant Sanyam Date: Wed, 19 Apr 2023 02:01:27 -0700 Subject: [PATCH] Add support for multiple order by (#387) --- plug-api/lib/query.ts | 42 +++++++++++++++++++++++------------ plugs/directive/parser.ts | 25 +++++++++++++++++---- plugs/directive/query.test.ts | 8 +++++-- 3 files changed, 55 insertions(+), 20 deletions(-) diff --git a/plug-api/lib/query.ts b/plug-api/lib/query.ts index cbc2d296..d0a38242 100644 --- a/plug-api/lib/query.ts +++ b/plug-api/lib/query.ts @@ -13,11 +13,25 @@ export type QueryFilter = { value: any; }; +export type QueryOrdering = { + orderBy: string; + orderDesc: boolean; +}; + export type ParsedQuery = { table: string; - orderBy?: string; - orderDesc?: boolean; limit?: number; + ordering: QueryOrdering[]; + /** @deprecated Please use ordering. + * Deprecated due to PR #387 + * Currently holds ordering[0] if exists + */ + orderBy?: string; + /** @deprecated Please use ordering. + * Deprecated due to PR #387 + * Currently holds ordering[0] if exists + */ + orderDesc?: boolean; filter: QueryFilter[]; select?: string[]; render?: string; @@ -97,22 +111,22 @@ export function applyQuery(parsedQuery: ParsedQuery, records: T[]): T[] { resultRecords.push(recordAny); } } - // Now the sorting - if (parsedQuery.orderBy) { - resultRecords = resultRecords.sort((a: any, b: any) => { - const orderBy = parsedQuery.orderBy!; - const orderDesc = parsedQuery.orderDesc!; - if (a[orderBy] === b[orderBy]) { - return 0; - } - if (a[orderBy] < b[orderBy]) { - return orderDesc ? 1 : -1; - } else { - return orderDesc ? -1 : 1; + if (parsedQuery.ordering.length > 0) { + resultRecords = resultRecords.sort((a: any, b: any) => { + for (const { orderBy, orderDesc } of parsedQuery.ordering) { + if (a[orderBy] < b[orderBy] || a[orderBy] === undefined) { + return orderDesc ? 1 : -1; + } + if (a[orderBy] > b[orderBy] || b[orderBy] === undefined) { + return orderDesc ? -1 : 1; + } + // Consider them equal. This way helps with comparing arrays (like tags) } + return 0; }); } + if (parsedQuery.limit) { resultRecords = resultRecords.slice(0, parsedQuery.limit); } diff --git a/plugs/directive/parser.ts b/plugs/directive/parser.ts index f656f78d..6006edb6 100644 --- a/plugs/directive/parser.ts +++ b/plugs/directive/parser.ts @@ -26,16 +26,33 @@ export function parseQuery(queryTree: ParseTree): ParsedQuery { const parsedQuery: ParsedQuery = { table: queryNode.children![0].children![0].text!, filter: [], + ordering: [], }; - const orderByNode = findNodeOfType(queryNode, "OrderClause"); - if (orderByNode) { + + const orderByNodes = collectNodesOfType(queryNode, "OrderClause"); + for (const orderByNode of orderByNodes) { const nameNode = findNodeOfType(orderByNode, "Name"); - parsedQuery.orderBy = nameNode!.children![0].text!; + const orderBy = nameNode!.children![0].text!; const orderNode = findNodeOfType(orderByNode, "OrderDirection"); - parsedQuery.orderDesc = orderNode + const orderDesc = orderNode ? orderNode.children![0].text! === "desc" : false; + parsedQuery.ordering.push({ orderBy, orderDesc }); } + /** + * @deprecated due to PR #387 + * We'll take the first ordering and send that as the deprecated + * fields orderBy and orderDesc. This way it will be backward + * Plugs using the old ParsedQuery. + * Remove this block completely when ParsedQuery no longer have + * those two fields + */ + if (parsedQuery.ordering.length > 0) { + parsedQuery.orderBy = parsedQuery.ordering[0].orderBy; + parsedQuery.orderDesc = parsedQuery.ordering[0].orderDesc; + } + /** @end-deprecation due to PR #387 */ + const limitNode = findNodeOfType(queryNode, "LimitClause"); if (limitNode) { const nameNode = findNodeOfType(limitNode, "Number"); diff --git a/plugs/directive/query.test.ts b/plugs/directive/query.test.ts index 83a3b48c..622525b2 100644 --- a/plugs/directive/query.test.ts +++ b/plugs/directive/query.test.ts @@ -26,8 +26,9 @@ Deno.test("Test parser", () => { `task where completed = false and dueDate <= "{{today}}" order by dueDate desc limit 5`, ); assertEquals(parsedQuery1.table, "task"); - assertEquals(parsedQuery1.orderBy, "dueDate"); - assertEquals(parsedQuery1.orderDesc, true); + assertEquals(parsedQuery1.ordering.length, 1); + assertEquals(parsedQuery1.ordering[0].orderBy, "dueDate"); + assertEquals(parsedQuery1.ordering[0].orderDesc, true); assertEquals(parsedQuery1.limit, 5); assertEquals(parsedQuery1.filter.length, 2); assertEquals(parsedQuery1.filter[0], { @@ -69,6 +70,7 @@ Deno.test("Test parser", () => { parseQuery(`gh-events where type in ["PushEvent", "somethingElse"]`), { table: "gh-events", + ordering: [], filter: [ { op: "in", @@ -81,12 +83,14 @@ Deno.test("Test parser", () => { assertEquals(parseQuery(`something render [[template/table]]`), { table: "something", + ordering: [], filter: [], render: "template/table", }); assertEquals(parseQuery(`something render "template/table"`), { table: "something", + ordering: [], filter: [], render: "template/table", });