From 6ba109222bc85e003b53edbd20c041301496bfaf Mon Sep 17 00:00:00 2001 From: Martin McKeaveney Date: Fri, 21 Aug 2020 17:05:26 +0100 Subject: [PATCH] custom filtering working, needs more test coverage --- .../database/DataTable/ViewDataTable.svelte | 6 +- .../DataTable/modals/CreateEditRecord.svelte | 25 +-- .../modals/RecordFieldControl.svelte | 38 +++- .../database/DataTable/popovers/Filter.svelte | 162 ++++++++++++++++++ packages/server/package.json | 4 +- .../__snapshots__/viewBuilder.spec.js.snap | 63 +++++++ .../view/tests/viewBuilder.spec.js | 39 +++++ .../src/api/controllers/view/viewBuilder.js | 69 +++++--- 8 files changed, 343 insertions(+), 63 deletions(-) create mode 100644 packages/builder/src/components/database/DataTable/popovers/Filter.svelte create mode 100644 packages/server/src/api/controllers/view/tests/__snapshots__/viewBuilder.spec.js.snap create mode 100644 packages/server/src/api/controllers/view/tests/viewBuilder.spec.js diff --git a/packages/builder/src/components/database/DataTable/ViewDataTable.svelte b/packages/builder/src/components/database/DataTable/ViewDataTable.svelte index 838512a872..64088da551 100644 --- a/packages/builder/src/components/database/DataTable/ViewDataTable.svelte +++ b/packages/builder/src/components/database/DataTable/ViewDataTable.svelte @@ -17,6 +17,7 @@ import EditRowPopover from "./popovers/EditRow.svelte" import CalculationPopover from "./popovers/Calculate.svelte" import GroupByPopover from "./popovers/GroupBy.svelte" + import FilterPopover from "./popovers/Filter.svelte" let COLUMNS = [ { @@ -54,8 +55,8 @@ let data = [] - $: ({ name, groupBy } = view) - $: !name.startsWith("all_") && fetchViewData(name, groupBy) + $: ({ name, groupBy, filters } = view) + $: !name.startsWith("all_") && filters && fetchViewData(name, groupBy) async function fetchViewData(name, groupBy) { let QUERY_VIEW_URL = `/api/views/${name}?stats=true` @@ -69,6 +70,7 @@ +
diff --git a/packages/builder/src/components/database/DataTable/modals/CreateEditRecord.svelte b/packages/builder/src/components/database/DataTable/modals/CreateEditRecord.svelte index 3eb7a4a149..b215a0583a 100644 --- a/packages/builder/src/components/database/DataTable/modals/CreateEditRecord.svelte +++ b/packages/builder/src/components/database/DataTable/modals/CreateEditRecord.svelte @@ -19,25 +19,6 @@ ? Object.entries($backendUiStore.selectedModel.schema) : [] - const isSelect = meta => - meta.type === "string" && - meta.constraints && - meta.constraints.inclusion && - meta.constraints.inclusion.length > 0 - - function determineInputType(meta) { - if (meta.type === "datetime") return "date" - if (meta.type === "number") return "number" - if (meta.type === "boolean") return "checkbox" - if (isSelect(meta)) return "select" - - return "text" - } - - function determineOptions(meta) { - return isSelect(meta) ? meta.constraints.inclusion : [] - } - async function saveRecord() { const recordResponse = await api.saveRecord( { @@ -73,11 +54,7 @@ linkName={meta.name} modelId={meta.modelId} /> {:else} - + {/if} {/each} diff --git a/packages/builder/src/components/database/DataTable/modals/RecordFieldControl.svelte b/packages/builder/src/components/database/DataTable/modals/RecordFieldControl.svelte index 638625a830..8c3a6b858e 100644 --- a/packages/builder/src/components/database/DataTable/modals/RecordFieldControl.svelte +++ b/packages/builder/src/components/database/DataTable/modals/RecordFieldControl.svelte @@ -1,10 +1,32 @@ {#if type === 'select'} - @@ -30,12 +52,12 @@ {:else} {#if type === 'checkbox'} - + {/if} + import { + Popover, + TextButton, + Button, + Icon, + Input, + Select, + } from "@budibase/bbui" + import { backendUiStore } from "builderStore" + import { notifier } from "builderStore/store/notifications" + import CreateEditRecord from "../modals/CreateEditRecord.svelte" + + const CONDITIONS = [ + { + name: "Equals", + key: "EQUALS", + }, + { + name: "Less Than", + key: "LT", + }, + { + name: "Less Than Or Equal", + key: "LTE", + }, + { + name: "More Than", + key: "MT", + }, + { + name: "More Than Or Equal", + key: "MTE", + }, + { + name: "Contains", + key: "CONTAINS", + }, + ] + + const CONJUNCTIONS = [ + { + name: "Or", + key: "OR", + }, + { + name: "And", + key: "AND", + }, + ] + + export let view = {} + + let anchor + let dropdown + let filters = view.filters + + $: viewModel = $backendUiStore.models.find( + ({ _id }) => _id === $backendUiStore.selectedView.modelId + ) + $: fields = viewModel && Object.keys(viewModel.schema) + + function saveView() { + view.filters = filters + backendUiStore.actions.views.save(view) + notifier.success(`View ${view.name} saved.`) + dropdown.hide() + } + + function removeFilter(idx) { + filters.splice(idx, 1) + filters = filters + } + + function addFilter() { + filters = [...filters, {}] + } + + +
+ + + Filter + +
+ +
Filter
+
+ {#each filters as filter, idx} + {#if idx === 0} +

Where

+ {:else} + + {/if} + + + + removeFilter(idx)} /> + {/each} +
+
+ +
+ + +
+
+
+ + diff --git a/packages/server/package.json b/packages/server/package.json index b3baaf7852..5452a10208 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -23,8 +23,8 @@ } }, "scripts": { - "test": "jest routes --runInBand", - "test:integration": "jest workflow --runInBand", + "test": "jest --testPathIgnorePatterns=routes && npm run test:integration", + "test:integration": "jest routes --runInBand", "test:watch": "jest --watch", "initialise": "node ../cli/bin/budi init -q", "run:docker": "node src/index", diff --git a/packages/server/src/api/controllers/view/tests/__snapshots__/viewBuilder.spec.js.snap b/packages/server/src/api/controllers/view/tests/__snapshots__/viewBuilder.spec.js.snap new file mode 100644 index 0000000000..758bf7e623 --- /dev/null +++ b/packages/server/src/api/controllers/view/tests/__snapshots__/viewBuilder.spec.js.snap @@ -0,0 +1,63 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`viewBuilder Filter creates a view with multiple filters and conjunctions 1`] = ` +Object { + "map": "function (doc) { + if (doc.modelId === \\"14f1c4e94d6a47b682ce89d35d4c78b0\\" && doc[\\"Name\\"] === \\"Test\\" || doc[\\"Yes\\"] > \\"Value\\") { + emit(doc._id); + } + }", + "meta": Object { + "field": undefined, + "filters": Array [ + Object { + "condition": "EQUALS", + "key": "Name", + "value": "Test", + }, + Object { + "condition": "MT", + "conjunction": "OR", + "key": "Yes", + "value": "Value", + }, + ], + "groupBy": undefined, + "modelId": "14f1c4e94d6a47b682ce89d35d4c78b0", + "schema": Object { + "avg": "number", + "count": "number", + "max": "number", + "min": "number", + "sum": "number", + "sumsqr": "number", + }, + }, + "reduce": "_stats", +} +`; + +exports[`viewBuilder Group By creates a view emitting the group by field 1`] = ` +Object { + "map": "function (doc) { + if (doc.modelId === \\"14f1c4e94d6a47b682ce89d35d4c78b0\\" ) { + emit(doc[\\"age\\"], doc[\\"score\\"]); + } + }", + "meta": Object { + "field": "score", + "filters": Array [], + "groupBy": "age", + "modelId": "14f1c4e94d6a47b682ce89d35d4c78b0", + "schema": Object { + "avg": "number", + "count": "number", + "max": "number", + "min": "number", + "sum": "number", + "sumsqr": "number", + }, + }, + "reduce": "_stats", +} +`; diff --git a/packages/server/src/api/controllers/view/tests/viewBuilder.spec.js b/packages/server/src/api/controllers/view/tests/viewBuilder.spec.js new file mode 100644 index 0000000000..4f36d180ba --- /dev/null +++ b/packages/server/src/api/controllers/view/tests/viewBuilder.spec.js @@ -0,0 +1,39 @@ +const statsViewTemplate = require("../viewBuilder"); + +describe("viewBuilder", () => { + + describe("Filter", () => { + it("creates a view with multiple filters and conjunctions", () => { + expect(statsViewTemplate({ + "name": "yeety", + "modelId": "14f1c4e94d6a47b682ce89d35d4c78b0", + "filters": [{ + "value": "Test", + "condition": "EQUALS", + "key": "Name" + }, { + "value": "Value", + "condition": "MT", + "key": "Yes", + "conjunction": "OR" + }] + })).toMatchSnapshot() + }) + }) + + describe("Calculate", () => { + + }) + + describe("Group By", () => { + it("creates a view emitting the group by field", () => { + expect(statsViewTemplate({ + "name": "Test Scores Grouped By Age", + "modelId": "14f1c4e94d6a47b682ce89d35d4c78b0", + "groupBy": "age", + "field": "score", + "filters": [], + })).toMatchSnapshot() + }) + }) +}); \ No newline at end of file diff --git a/packages/server/src/api/controllers/view/viewBuilder.js b/packages/server/src/api/controllers/view/viewBuilder.js index 2c2b2f9b62..779a38acd5 100644 --- a/packages/server/src/api/controllers/view/viewBuilder.js +++ b/packages/server/src/api/controllers/view/viewBuilder.js @@ -4,42 +4,55 @@ const TOKEN_MAP = { LTE: "<=", MT: ">", MTE: ">=", - CONTAINS: "includes()", + CONTAINS: "includes", AND: "&&", - OR: "||" + OR: "||", } -function parseFilters(filters) { - const expression = filters.map(filter => { - if (filter.conjunction) return TOKEN_MAP[filter.conjunction]; - - return `doc["${filter.key}"] ${TOKEN_MAP[filter.condition]} "${filter.value}"` - }) - +/** + * Iterates through the array of filters to create a JS + * expression that gets used in a CouchDB view. + * @param {Array} filters - an array of filter objects + * @returns {String} JS Expression + */ +function parseFilterExpression(filters) { + const expression = [] + + for (let filter of filters) { + if (filter.conjunction) expression.push(TOKEN_MAP[filter.conjunction]); + + if (filter.condition === "CONTAINS") { + expression.push( + `doc["${filter.key}"].${TOKEN_MAP[filter.condition]}("${ + filter.value + }")`) + return + } + + expression.push(`doc["${filter.key}"] ${TOKEN_MAP[filter.condition]} "${ + filter.value + }"`) + } + return expression.join(" ") } -function statsViewTemplate({ field, modelId, groupBy }) { +function parseEmitExpression(field, groupBy) { + if (field) return `emit(doc["${groupBy || "_id"}"], doc["${field}"]);` + return `emit(doc._id);` +} + +function statsViewTemplate({ field, modelId, groupBy, filters = [] }) { + const filterExpression = parseFilterExpression(filters) + + const emitExpression = parseEmitExpression(field, groupBy) + return { meta: { field, modelId, groupBy, - filter: [ - { - key: "Status", - condition: "Equals", - value: "VIP", - }, - { - conjunction: "AND" - }, - { - key: "Status", - condition: "Equals", - value: "VIP", - } - ], + filters, schema: { sum: "number", min: "number", @@ -50,8 +63,10 @@ function statsViewTemplate({ field, modelId, groupBy }) { }, }, map: `function (doc) { - if (doc.modelId === "${modelId}") { - emit(doc["${groupBy || "_id"}"], doc["${field}"]); + if (doc.modelId === "${modelId}" ${ + filterExpression ? `&& ${filterExpression}` : "" + }) { + ${emitExpression} } }`, reduce: "_stats",