mirror of https://github.com/Budibase/budibase.git
committed by
GitHub
106 changed files with 3550 additions and 604 deletions
@ -0,0 +1,126 @@ |
|||
<script> |
|||
export let width = "100" |
|||
export let height = "100" |
|||
</script> |
|||
|
|||
<svg |
|||
version="1.1" |
|||
id="Layer_1" |
|||
xmlns="http://www.w3.org/2000/svg" |
|||
xmlns:xlink="http://www.w3.org/1999/xlink" |
|||
x="0px" |
|||
y="0px" |
|||
viewBox="0 0 48 48" |
|||
style="enable-background:new 0 0 48 48;" |
|||
xml:space="preserve" |
|||
{height} |
|||
{width} |
|||
> |
|||
<style type="text/css"> |
|||
.st0 { |
|||
fill: #393c44; |
|||
} |
|||
.st1 { |
|||
fill: #ffffff; |
|||
} |
|||
.st2 { |
|||
fill: #4285f4; |
|||
} |
|||
</style> |
|||
<rect x="-152.17" y="-24.17" class="st0" width="96.17" height="96.17" /> |
|||
<path |
|||
class="st1" |
|||
d="M-83.19,48h-41.79c-1.76,0-3.19-1.43-3.19-3.19V3.02c0-1.76,1.43-3.19,3.19-3.19h41.79 |
|||
c1.76,0,3.19,1.43,3.19,3.19v41.79C-80,46.57-81.43,48-83.19,48z" |
|||
/> |
|||
<g> |
|||
<g> |
|||
<path |
|||
class="st0" |
|||
d="M-99.62,12.57v9.94c1.15-1.21,2.59-1.81,4.32-1.81c1.03,0,1.97,0.19,2.82,0.58c0.86,0.39,1.59,0.91,2.19,1.57 |
|||
c0.6,0.66,1.08,1.43,1.42,2.32c0.34,0.89,0.51,1.84,0.51,2.85c0,1.03-0.18,1.99-0.53,2.89c-0.35,0.9-0.84,1.68-1.47,2.35 |
|||
c-0.63,0.67-1.37,1.19-2.23,1.58c-0.86,0.39-1.78,0.58-2.77,0.58c-1.8,0-3.22-0.66-4.27-1.97V35h-4.89V12.57H-99.62z |
|||
M-93.46,28.11c0-0.43-0.08-0.84-0.24-1.23c-0.16-0.39-0.39-0.72-0.68-1.01c-0.29-0.29-0.62-0.52-1-0.69 |
|||
c-0.38-0.17-0.79-0.26-1.24-0.26c-0.43,0-0.84,0.08-1.22,0.24c-0.38,0.16-0.71,0.39-0.99,0.68c-0.28,0.29-0.5,0.63-0.68,1.01 |
|||
c-0.17,0.39-0.26,0.8-0.26,1.23c0,0.43,0.08,0.84,0.24,1.22c0.16,0.38,0.39,0.71,0.68,0.99c0.29,0.28,0.63,0.5,1.01,0.68 |
|||
c0.39,0.17,0.8,0.26,1.23,0.26c0.43,0,0.84-0.08,1.22-0.24c0.38-0.16,0.71-0.39,0.99-0.68c0.28-0.29,0.5-0.62,0.68-1 |
|||
C-93.55,28.92-93.46,28.52-93.46,28.11z" |
|||
/> |
|||
</g> |
|||
<g> |
|||
<path |
|||
class="st0" |
|||
d="M-114.76,12.57v9.94c1.15-1.21,2.59-1.81,4.32-1.81c1.03,0,1.97,0.19,2.82,0.58 |
|||
c0.86,0.39,1.59,0.91,2.19,1.57c0.6,0.66,1.08,1.43,1.42,2.32c0.34,0.89,0.51,1.84,0.51,2.85c0,1.03-0.18,1.99-0.53,2.89 |
|||
c-0.35,0.9-0.84,1.68-1.47,2.35c-0.63,0.67-1.37,1.19-2.23,1.58c-0.86,0.39-1.78,0.58-2.77,0.58c-1.8,0-3.22-0.66-4.27-1.97V35 |
|||
h-4.89V12.57H-114.76z M-108.6,28.11c0-0.43-0.08-0.84-0.24-1.23c-0.16-0.39-0.39-0.72-0.68-1.01c-0.29-0.29-0.62-0.52-1-0.69 |
|||
c-0.38-0.17-0.79-0.26-1.24-0.26c-0.43,0-0.84,0.08-1.22,0.24c-0.38,0.16-0.71,0.39-0.99,0.68c-0.28,0.29-0.5,0.63-0.68,1.01 |
|||
c-0.17,0.39-0.26,0.8-0.26,1.23c0,0.43,0.08,0.84,0.24,1.22c0.16,0.38,0.39,0.71,0.68,0.99c0.29,0.28,0.63,0.5,1.01,0.68 |
|||
c0.39,0.17,0.8,0.26,1.23,0.26c0.43,0,0.84-0.08,1.22-0.24c0.38-0.16,0.71-0.39,0.99-0.68c0.28-0.29,0.5-0.62,0.68-1 |
|||
C-108.68,28.92-108.6,28.52-108.6,28.11z" |
|||
/> |
|||
</g> |
|||
</g> |
|||
<path |
|||
class="st2" |
|||
d="M44.81,159H3.02c-1.76,0-3.19-1.43-3.19-3.19v-41.79c0-1.76,1.43-3.19,3.19-3.19h41.79 |
|||
c1.76,0,3.19,1.43,3.19,3.19v41.79C48,157.57,46.57,159,44.81,159z" |
|||
/> |
|||
<g> |
|||
<g> |
|||
<path |
|||
class="st1" |
|||
d="M28.38,123.57v9.94c1.15-1.21,2.59-1.81,4.32-1.81c1.03,0,1.97,0.19,2.82,0.58c0.86,0.39,1.59,0.91,2.19,1.57 |
|||
c0.6,0.66,1.08,1.43,1.42,2.32c0.34,0.89,0.51,1.84,0.51,2.85c0,1.03-0.18,1.99-0.53,2.89c-0.35,0.9-0.84,1.68-1.47,2.35 |
|||
c-0.63,0.67-1.37,1.19-2.23,1.58c-0.86,0.39-1.78,0.58-2.77,0.58c-1.8,0-3.22-0.66-4.27-1.97V146h-4.89v-22.43H28.38z |
|||
M34.54,139.11c0-0.43-0.08-0.84-0.24-1.23c-0.16-0.39-0.39-0.72-0.68-1.01c-0.29-0.29-0.62-0.52-1-0.69 |
|||
c-0.38-0.17-0.79-0.26-1.24-0.26c-0.43,0-0.84,0.08-1.22,0.24c-0.38,0.16-0.71,0.39-0.99,0.68c-0.28,0.29-0.5,0.63-0.68,1.01 |
|||
c-0.17,0.39-0.26,0.8-0.26,1.23c0,0.43,0.08,0.84,0.24,1.22c0.16,0.38,0.39,0.71,0.68,0.99c0.29,0.28,0.63,0.5,1.01,0.68 |
|||
c0.39,0.17,0.8,0.26,1.23,0.26c0.43,0,0.84-0.08,1.22-0.24c0.38-0.16,0.71-0.39,0.99-0.68c0.28-0.29,0.5-0.62,0.68-1 |
|||
C34.45,139.92,34.54,139.52,34.54,139.11z" |
|||
/> |
|||
</g> |
|||
<g> |
|||
<path |
|||
class="st1" |
|||
d="M13.24,123.57v9.94c1.15-1.21,2.59-1.81,4.32-1.81c1.03,0,1.97,0.19,2.82,0.58c0.86,0.39,1.59,0.91,2.19,1.57 |
|||
c0.6,0.66,1.08,1.43,1.42,2.32c0.34,0.89,0.51,1.84,0.51,2.85c0,1.03-0.18,1.99-0.53,2.89c-0.35,0.9-0.84,1.68-1.47,2.35 |
|||
c-0.63,0.67-1.37,1.19-2.23,1.58c-0.86,0.39-1.78,0.58-2.77,0.58c-1.8,0-3.22-0.66-4.27-1.97V146H8.35v-22.43H13.24z M19.4,139.11 |
|||
c0-0.43-0.08-0.84-0.24-1.23c-0.16-0.39-0.39-0.72-0.68-1.01c-0.29-0.29-0.62-0.52-1-0.69c-0.38-0.17-0.79-0.26-1.24-0.26 |
|||
c-0.43,0-0.84,0.08-1.22,0.24c-0.38,0.16-0.71,0.39-0.99,0.68c-0.28,0.29-0.5,0.63-0.68,1.01c-0.17,0.39-0.26,0.8-0.26,1.23 |
|||
c0,0.43,0.08,0.84,0.24,1.22c0.16,0.38,0.39,0.71,0.68,0.99c0.29,0.28,0.63,0.5,1.01,0.68c0.39,0.17,0.8,0.26,1.23,0.26 |
|||
c0.43,0,0.84-0.08,1.22-0.24c0.38-0.16,0.71-0.39,0.99-0.68c0.28-0.29,0.5-0.62,0.68-1C19.32,139.92,19.4,139.52,19.4,139.11z" |
|||
/> |
|||
</g> |
|||
</g> |
|||
<g> |
|||
<path |
|||
class="st0" |
|||
d="M44,48H4c-2.21,0-4-1.79-4-4V4c0-2.21,1.79-4,4-4h40c2.21,0,4,1.79,4,4v40C48,46.21,46.21,48,44,48z" |
|||
/> |
|||
<g> |
|||
<path |
|||
class="st1" |
|||
d="M28.48,12v10.44c1.18-1.27,2.65-1.9,4.42-1.9c1.05,0,2.01,0.2,2.89,0.61c0.87,0.41,1.62,0.96,2.24,1.65 |
|||
c0.62,0.69,1.1,1.5,1.45,2.44c0.35,0.94,0.52,1.93,0.52,2.99c0,1.08-0.18,2.09-0.54,3.04c-0.36,0.95-0.86,1.77-1.51,2.47 |
|||
c-0.64,0.7-1.4,1.25-2.28,1.66C34.8,35.8,33.86,36,32.84,36c-1.84,0-3.3-0.69-4.37-2.07v1.62h-5V12H28.48z M34.78,28.31 |
|||
c0-0.45-0.08-0.88-0.25-1.29c-0.17-0.41-0.4-0.76-0.69-1.06c-0.3-0.3-0.64-0.54-1.02-0.72c-0.39-0.18-0.81-0.27-1.27-0.27 |
|||
c-0.44,0-0.86,0.09-1.24,0.26c-0.39,0.17-0.72,0.41-1.01,0.71c-0.29,0.3-0.52,0.66-0.69,1.06c-0.18,0.41-0.26,0.84-0.26,1.29 |
|||
s0.08,0.88,0.25,1.28c0.17,0.4,0.4,0.74,0.69,1.04c0.29,0.29,0.64,0.53,1.04,0.71c0.4,0.18,0.82,0.27,1.26,0.27 |
|||
c0.44,0,0.86-0.09,1.24-0.26c0.39-0.17,0.72-0.41,1.01-0.71c0.29-0.3,0.52-0.65,0.69-1.05C34.69,29.16,34.78,28.75,34.78,28.31z" |
|||
/> |
|||
</g> |
|||
<g> |
|||
<path |
|||
class="st1" |
|||
d="M13,12v10.44c1.18-1.27,2.65-1.9,4.42-1.9c1.05,0,2.01,0.2,2.89,0.61c0.87,0.41,1.62,0.96,2.24,1.65 |
|||
c0.62,0.69,1.1,1.5,1.45,2.44c0.35,0.94,0.52,1.93,0.52,2.99c0,1.08-0.18,2.09-0.54,3.04c-0.36,0.95-0.86,1.77-1.51,2.47 |
|||
c-0.64,0.7-1.4,1.25-2.28,1.66C19.32,35.8,18.38,36,17.37,36c-1.84,0-3.3-0.69-4.37-2.07v1.62H8V12H13z M19.3,28.31 |
|||
c0-0.45-0.08-0.88-0.25-1.29c-0.17-0.41-0.4-0.76-0.69-1.06c-0.3-0.3-0.64-0.54-1.02-0.72c-0.39-0.18-0.81-0.27-1.27-0.27 |
|||
c-0.44,0-0.86,0.09-1.24,0.26c-0.39,0.17-0.72,0.41-1.01,0.71c-0.29,0.3-0.52,0.66-0.69,1.06c-0.18,0.41-0.26,0.84-0.26,1.29 |
|||
s0.08,0.88,0.25,1.28c0.17,0.4,0.4,0.74,0.69,1.04c0.29,0.29,0.64,0.53,1.04,0.71c0.4,0.18,0.82,0.27,1.26,0.27 |
|||
c0.44,0,0.86-0.09,1.24-0.26c0.39-0.17,0.72-0.41,1.01-0.71c0.29-0.3,0.52-0.65,0.69-1.05C19.21,29.16,19.3,28.75,19.3,28.31z" |
|||
/> |
|||
</g> |
|||
</g> |
|||
</svg> |
|||
@ -0,0 +1,17 @@ |
|||
<script> |
|||
import { Icon } from "@budibase/bbui" |
|||
</script> |
|||
|
|||
<a target="_blank" href="https://github.com/Budibase/budibase/discussions"> |
|||
<Icon hoverable name="Help" size="XXL" /> |
|||
</a> |
|||
|
|||
<style> |
|||
a { |
|||
color: inherit; |
|||
position: absolute; |
|||
bottom: var(--spacing-m); |
|||
right: var(--spacing-m); |
|||
border-radius: 55%; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,14 @@ |
|||
<script> |
|||
import { Body } from "@budibase/bbui" |
|||
</script> |
|||
|
|||
<div class="root"> |
|||
<Body size="S">This action doesn't require any additional settings.</Body> |
|||
</div> |
|||
|
|||
<style> |
|||
.root { |
|||
max-width: 800px; |
|||
margin: 0 auto; |
|||
} |
|||
</style> |
|||
@ -1,13 +1 @@ |
|||
<script> |
|||
import { params } from "@roxi/routify" |
|||
import { queries } from "stores/backend" |
|||
|
|||
if ($params.query) { |
|||
const query = $queries.list.find(m => m._id === $params.query) |
|||
if (query) { |
|||
queries.select(query) |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<slot /> |
|||
|
|||
@ -0,0 +1,81 @@ |
|||
<script> |
|||
import { Button, Heading, Body, Layout, Modal, Divider } from "@budibase/bbui" |
|||
import CreateTableModal from "components/backend/TableNavigator/modals/CreateTableModal.svelte" |
|||
import ICONS from "components/backend/DatasourceNavigator/icons" |
|||
import { tables } from "stores/backend" |
|||
import { goto } from "@roxi/routify" |
|||
|
|||
let modal |
|||
</script> |
|||
|
|||
<Modal bind:this={modal}> |
|||
<CreateTableModal /> |
|||
</Modal> |
|||
|
|||
<section> |
|||
<Layout> |
|||
<header> |
|||
<svelte:component this={ICONS.BUDIBASE} height="26" width="26" /> |
|||
<Heading size="M">Budibase Internal</Heading> |
|||
</header> |
|||
<Body size="S" grey lh |
|||
>Budibase internal tables are part of your app, the data will be stored in |
|||
your apps context.</Body |
|||
> |
|||
<Divider /> |
|||
<Heading size="S">Tables</Heading> |
|||
<div class="table-list"> |
|||
{#each $tables.list.filter(table => table.type !== "external") as table} |
|||
<div |
|||
class="table-list-item" |
|||
on:click={$goto(`../../table/${table._id}`)} |
|||
> |
|||
<Body size="S">{table.name}</Body> |
|||
{#if table.primaryDisplay} |
|||
<Body size="S">display column: {table.primaryDisplay}</Body> |
|||
{/if} |
|||
</div> |
|||
{/each} |
|||
</div> |
|||
<div> |
|||
<Button cta on:click={modal.show}>Create new table</Button> |
|||
</div> |
|||
</Layout> |
|||
</section> |
|||
|
|||
<style> |
|||
section { |
|||
margin: 0 auto; |
|||
width: 640px; |
|||
} |
|||
|
|||
header { |
|||
margin: 0 0 var(--spacing-xs) 0; |
|||
display: flex; |
|||
gap: var(--spacing-l); |
|||
align-items: center; |
|||
} |
|||
|
|||
.table-list { |
|||
display: flex; |
|||
flex-direction: column; |
|||
gap: var(--spacing-m); |
|||
} |
|||
|
|||
.table-list-item { |
|||
border-radius: var(--border-radius-m); |
|||
background: var(--background); |
|||
border: var(--border-dark); |
|||
display: grid; |
|||
grid-template-columns: 2fr 0.75fr 20px; |
|||
align-items: center; |
|||
padding: var(--spacing-m); |
|||
gap: var(--layout-xs); |
|||
transition: 200ms background ease; |
|||
} |
|||
|
|||
.table-list-item:hover { |
|||
background: var(--grey-1); |
|||
cursor: pointer; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,13 @@ |
|||
<script> |
|||
import { params } from "@roxi/routify" |
|||
import { tables } from "stores/backend" |
|||
|
|||
if ($params.selectedTable) { |
|||
const table = $tables.list.find(m => m._id === $params.selectedTable) |
|||
if (table) { |
|||
tables.select(table) |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<slot /> |
|||
@ -0,0 +1,16 @@ |
|||
<script> |
|||
import TableDataTable from "components/backend/DataTable/DataTable.svelte" |
|||
import { tables, database } from "stores/backend" |
|||
</script> |
|||
|
|||
{#if $database?._id && $tables?.selected?.name} |
|||
<TableDataTable /> |
|||
{:else}<i>Create your first table to start building</i>{/if} |
|||
|
|||
<style> |
|||
i { |
|||
font-size: var(--font-size-m); |
|||
color: var(--grey-5); |
|||
margin-top: 2px; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,10 @@ |
|||
<script> |
|||
import { params } from "@roxi/routify" |
|||
import RelationshipDataTable from "components/backend/DataTable/RelationshipDataTable.svelte" |
|||
</script> |
|||
|
|||
<RelationshipDataTable |
|||
tableId={$params.selectedTable} |
|||
rowId={$params.selectedRow} |
|||
fieldName={decodeURI($params.selectedField)} |
|||
/> |
|||
@ -0,0 +1,6 @@ |
|||
<script> |
|||
import { goto } from "@roxi/routify" |
|||
$goto("../../") |
|||
</script> |
|||
|
|||
<!-- routify:options index=false --> |
|||
@ -0,0 +1,6 @@ |
|||
<script> |
|||
import { goto } from "@roxi/routify" |
|||
$goto("../") |
|||
</script> |
|||
|
|||
<!-- routify:options index=false --> |
|||
@ -0,0 +1,19 @@ |
|||
<script> |
|||
import { tables } from "stores/backend" |
|||
import { goto, leftover } from "@roxi/routify" |
|||
import { onMount } from "svelte" |
|||
|
|||
onMount(async () => { |
|||
// navigate to first table in list, if not already selected |
|||
// and this is the final url (i.e. no selectedTable) |
|||
if ( |
|||
!$leftover && |
|||
$tables.list.length > 0 |
|||
// (!$tables.selected || !$tables.selected._id) |
|||
) { |
|||
$goto(`./${$tables.list[0]._id}`) |
|||
} |
|||
}) |
|||
</script> |
|||
|
|||
<slot /> |
|||
@ -0,0 +1,21 @@ |
|||
<script> |
|||
import { goto } from "@roxi/routify" |
|||
import { onMount } from "svelte" |
|||
import { tables } from "stores/backend" |
|||
|
|||
onMount(async () => { |
|||
$tables.list.length > 0 && $goto(`./${$tables.list[0]._id}`) |
|||
}) |
|||
</script> |
|||
|
|||
{#if $tables.list.length === 0} |
|||
<i>Create your first table to start building</i> |
|||
{:else}<i>Select a table to edit</i>{/if} |
|||
|
|||
<style> |
|||
i { |
|||
font-size: var(--font-size-m); |
|||
color: var(--grey-5); |
|||
margin-top: 2px; |
|||
} |
|||
</style> |
|||
File diff suppressed because it is too large
@ -0,0 +1,21 @@ |
|||
# Use root/example as user/password credentials |
|||
version: '3.1' |
|||
|
|||
services: |
|||
|
|||
db: |
|||
image: mysql |
|||
restart: always |
|||
command: --init-file /data/application/init.sql --default-authentication-plugin=mysql_native_password |
|||
volumes: |
|||
- ./init.sql:/data/application/init.sql |
|||
environment: |
|||
MYSQL_ROOT_PASSWORD: root |
|||
ports: |
|||
- 3306:3306 |
|||
|
|||
adminer: |
|||
image: adminer |
|||
restart: always |
|||
ports: |
|||
- 8080:8080 |
|||
@ -0,0 +1,9 @@ |
|||
CREATE DATABASE IF NOT EXISTS main; |
|||
USE main; |
|||
CREATE TABLE Persons ( |
|||
PersonID int NOT NULL PRIMARY KEY, |
|||
LastName varchar(255), |
|||
FirstName varchar(255), |
|||
Address varchar(255), |
|||
City varchar(255) |
|||
); |
|||
@ -0,0 +1,28 @@ |
|||
version: "3.8" |
|||
services: |
|||
db: |
|||
container_name: postgres |
|||
image: postgres |
|||
restart: always |
|||
environment: |
|||
POSTGRES_USER: root |
|||
POSTGRES_PASSWORD: root |
|||
POSTGRES_DB: main |
|||
ports: |
|||
- "5432:5432" |
|||
volumes: |
|||
#- pg_data:/var/lib/postgresql/data/ |
|||
- ./init.sql:/docker-entrypoint-initdb.d/init.sql |
|||
|
|||
pgadmin: |
|||
container_name: pgadmin |
|||
image: dpage/pgadmin4 |
|||
restart: always |
|||
environment: |
|||
PGADMIN_DEFAULT_EMAIL: root@root.com |
|||
PGADMIN_DEFAULT_PASSWORD: root |
|||
ports: |
|||
- "5050:80" |
|||
|
|||
#volumes: |
|||
# pg_data: |
|||
@ -0,0 +1,9 @@ |
|||
SELECT 'CREATE DATABASE main' |
|||
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'main')\gexec |
|||
CREATE TABLE Persons ( |
|||
PersonID int NOT NULL PRIMARY KEY, |
|||
LastName varchar(255), |
|||
FirstName varchar(255), |
|||
Address varchar(255), |
|||
City varchar(255) |
|||
); |
|||
@ -0,0 +1,276 @@ |
|||
const { makeExternalQuery } = require("./utils") |
|||
const { DataSourceOperation, SortDirection } = require("../../../constants") |
|||
const { getExternalTable } = require("../table/utils") |
|||
const { |
|||
breakExternalTableId, |
|||
generateRowIdField, |
|||
breakRowIdField, |
|||
} = require("../../../integrations/utils") |
|||
const { cloneDeep } = require("lodash/fp") |
|||
|
|||
function inputProcessing(row, table) { |
|||
if (!row) { |
|||
return row |
|||
} |
|||
let newRow = {} |
|||
for (let key of Object.keys(table.schema)) { |
|||
// currently excludes empty strings
|
|||
if (row[key]) { |
|||
newRow[key] = row[key] |
|||
} |
|||
} |
|||
return newRow |
|||
} |
|||
|
|||
function generateIdForRow(row, table) { |
|||
if (!row) { |
|||
return |
|||
} |
|||
const primary = table.primary |
|||
// build id array
|
|||
let idParts = [] |
|||
for (let field of primary) { |
|||
idParts.push(row[field]) |
|||
} |
|||
return generateRowIdField(idParts) |
|||
} |
|||
|
|||
function outputProcessing(rows, table) { |
|||
// if no rows this is what is returned? Might be PG only
|
|||
if (rows[0].read === true) { |
|||
return [] |
|||
} |
|||
for (let row of rows) { |
|||
row._id = generateIdForRow(row, table) |
|||
row.tableId = table._id |
|||
row._rev = "rev" |
|||
} |
|||
return rows |
|||
} |
|||
|
|||
function buildFilters(id, filters, table) { |
|||
const primary = table.primary |
|||
// if passed in array need to copy for shifting etc
|
|||
let idCopy = cloneDeep(id) |
|||
if (filters) { |
|||
// need to map over the filters and make sure the _id field isn't present
|
|||
for (let filter of Object.values(filters)) { |
|||
if (filter._id) { |
|||
const parts = breakRowIdField(filter._id) |
|||
for (let field of primary) { |
|||
filter[field] = parts.shift() |
|||
} |
|||
} |
|||
// make sure this field doesn't exist on any filter
|
|||
delete filter._id |
|||
} |
|||
} |
|||
// there is no id, just use the user provided filters
|
|||
if (!idCopy || !table) { |
|||
return filters |
|||
} |
|||
// if used as URL parameter it will have been joined
|
|||
if (typeof idCopy === "string") { |
|||
idCopy = breakRowIdField(idCopy) |
|||
} |
|||
const equal = {} |
|||
for (let field of primary) { |
|||
// work through the ID and get the parts
|
|||
equal[field] = idCopy.shift() |
|||
} |
|||
return { |
|||
equal, |
|||
} |
|||
} |
|||
|
|||
async function handleRequest( |
|||
appId, |
|||
operation, |
|||
tableId, |
|||
{ id, row, filters, sort, paginate } = {} |
|||
) { |
|||
let { datasourceId, tableName } = breakExternalTableId(tableId) |
|||
const table = await getExternalTable(appId, datasourceId, tableName) |
|||
if (!table) { |
|||
throw `Unable to process query, table "${tableName}" not defined.` |
|||
} |
|||
// clean up row on ingress using schema
|
|||
filters = buildFilters(id, filters, table) |
|||
row = inputProcessing(row, table) |
|||
if ( |
|||
operation === DataSourceOperation.DELETE && |
|||
(filters == null || Object.keys(filters).length === 0) |
|||
) { |
|||
throw "Deletion must be filtered" |
|||
} |
|||
let json = { |
|||
endpoint: { |
|||
datasourceId, |
|||
entityId: tableName, |
|||
operation, |
|||
}, |
|||
resource: { |
|||
// not specifying any fields means "*"
|
|||
fields: [], |
|||
}, |
|||
filters, |
|||
sort, |
|||
paginate, |
|||
body: row, |
|||
// pass an id filter into extra, purely for mysql/returning
|
|||
extra: { |
|||
idFilter: buildFilters(id || generateIdForRow(row, table), {}, table), |
|||
}, |
|||
} |
|||
// can't really use response right now
|
|||
const response = await makeExternalQuery(appId, json) |
|||
// we searched for rows in someway
|
|||
if (operation === DataSourceOperation.READ && Array.isArray(response)) { |
|||
return outputProcessing(response, table) |
|||
} else { |
|||
row = outputProcessing(response, table)[0] |
|||
return { row, table } |
|||
} |
|||
} |
|||
|
|||
exports.patch = async ctx => { |
|||
const appId = ctx.appId |
|||
const inputs = ctx.request.body |
|||
const tableId = ctx.params.tableId |
|||
const id = breakRowIdField(inputs._id) |
|||
// don't save the ID to db
|
|||
delete inputs._id |
|||
return handleRequest(appId, DataSourceOperation.UPDATE, tableId, { |
|||
id, |
|||
row: inputs, |
|||
}) |
|||
} |
|||
|
|||
exports.save = async ctx => { |
|||
const appId = ctx.appId |
|||
const inputs = ctx.request.body |
|||
const tableId = ctx.params.tableId |
|||
return handleRequest(appId, DataSourceOperation.CREATE, tableId, { |
|||
row: inputs, |
|||
}) |
|||
} |
|||
|
|||
exports.fetchView = async ctx => { |
|||
// there are no views in external data sources, shouldn't ever be called
|
|||
// for now just fetch
|
|||
ctx.params.tableId = ctx.params.viewName.split("all_")[1] |
|||
return exports.fetch(ctx) |
|||
} |
|||
|
|||
exports.fetch = async ctx => { |
|||
const appId = ctx.appId |
|||
const tableId = ctx.params.tableId |
|||
return handleRequest(appId, DataSourceOperation.READ, tableId) |
|||
} |
|||
|
|||
exports.find = async ctx => { |
|||
const appId = ctx.appId |
|||
const id = ctx.params.rowId |
|||
const tableId = ctx.params.tableId |
|||
return handleRequest(appId, DataSourceOperation.READ, tableId, { |
|||
id, |
|||
}) |
|||
} |
|||
|
|||
exports.destroy = async ctx => { |
|||
const appId = ctx.appId |
|||
const tableId = ctx.params.tableId |
|||
const id = ctx.request.body._id |
|||
const { row } = await handleRequest( |
|||
appId, |
|||
DataSourceOperation.DELETE, |
|||
tableId, |
|||
{ |
|||
id, |
|||
} |
|||
) |
|||
return { response: { ok: true }, row } |
|||
} |
|||
|
|||
exports.bulkDestroy = async ctx => { |
|||
const appId = ctx.appId |
|||
const { rows } = ctx.request.body |
|||
const tableId = ctx.params.tableId |
|||
let promises = [] |
|||
for (let row of rows) { |
|||
promises.push( |
|||
handleRequest(appId, DataSourceOperation.DELETE, tableId, { |
|||
id: breakRowIdField(row._id), |
|||
}) |
|||
) |
|||
} |
|||
const responses = await Promise.all(promises) |
|||
return { response: { ok: true }, rows: responses.map(resp => resp.row) } |
|||
} |
|||
|
|||
exports.search = async ctx => { |
|||
const appId = ctx.appId |
|||
const tableId = ctx.params.tableId |
|||
const { paginate, query, ...params } = ctx.request.body |
|||
let { bookmark, limit } = params |
|||
if (!bookmark && paginate) { |
|||
bookmark = 1 |
|||
} |
|||
let paginateObj = {} |
|||
|
|||
if (paginate) { |
|||
paginateObj = { |
|||
// add one so we can track if there is another page
|
|||
limit: limit, |
|||
page: bookmark, |
|||
} |
|||
} else if (params && limit) { |
|||
paginateObj = { |
|||
limit: limit, |
|||
} |
|||
} |
|||
let sort |
|||
if (params.sort) { |
|||
const direction = |
|||
params.sortOrder === "descending" |
|||
? SortDirection.DESCENDING |
|||
: SortDirection.ASCENDING |
|||
sort = { |
|||
[params.sort]: direction, |
|||
} |
|||
} |
|||
const rows = await handleRequest(appId, DataSourceOperation.READ, tableId, { |
|||
filters: query, |
|||
sort, |
|||
paginate: paginateObj, |
|||
}) |
|||
let hasNextPage = false |
|||
if (paginate && rows.length === limit) { |
|||
const nextRows = await handleRequest( |
|||
appId, |
|||
DataSourceOperation.READ, |
|||
tableId, |
|||
{ |
|||
filters: query, |
|||
sort, |
|||
paginate: { |
|||
limit: 1, |
|||
page: bookmark * limit + 1, |
|||
}, |
|||
} |
|||
) |
|||
hasNextPage = nextRows.length > 0 |
|||
} |
|||
// need wrapper object for bookmarks etc when paginating
|
|||
return { rows, hasNextPage, bookmark: bookmark + 1 } |
|||
} |
|||
|
|||
exports.validate = async () => { |
|||
// can't validate external right now - maybe in future
|
|||
return { valid: true } |
|||
} |
|||
|
|||
exports.fetchEnrichedRow = async () => { |
|||
// TODO: How does this work
|
|||
throw "Not Implemented" |
|||
} |
|||
@ -0,0 +1,138 @@ |
|||
const internal = require("./internal") |
|||
const external = require("./external") |
|||
const { isExternalTable } = require("../../../integrations/utils") |
|||
|
|||
function pickApi(tableId) { |
|||
if (isExternalTable(tableId)) { |
|||
return external |
|||
} |
|||
return internal |
|||
} |
|||
|
|||
function getTableId(ctx) { |
|||
if (ctx.request.body && ctx.request.body.tableId) { |
|||
return ctx.request.body.tableId |
|||
} |
|||
if (ctx.params && ctx.params.tableId) { |
|||
return ctx.params.tableId |
|||
} |
|||
if (ctx.params && ctx.params.viewName) { |
|||
return ctx.params.viewName |
|||
} |
|||
} |
|||
|
|||
exports.patch = async ctx => { |
|||
const appId = ctx.appId |
|||
const tableId = getTableId(ctx) |
|||
const body = ctx.request.body |
|||
// if it doesn't have an _id then its save
|
|||
if (body && !body._id) { |
|||
return exports.save(ctx) |
|||
} |
|||
try { |
|||
const { row, table } = await pickApi(tableId).patch(ctx) |
|||
ctx.status = 200 |
|||
ctx.eventEmitter && |
|||
ctx.eventEmitter.emitRow(`row:update`, appId, row, table) |
|||
ctx.message = `${table.name} updated successfully.` |
|||
ctx.body = row |
|||
} catch (err) { |
|||
ctx.throw(400, err) |
|||
} |
|||
} |
|||
|
|||
exports.save = async function (ctx) { |
|||
const appId = ctx.appId |
|||
const tableId = getTableId(ctx) |
|||
const body = ctx.request.body |
|||
// if it has an ID already then its a patch
|
|||
if (body && body._id) { |
|||
return exports.patch(ctx) |
|||
} |
|||
try { |
|||
const { row, table } = await pickApi(tableId).save(ctx) |
|||
ctx.status = 200 |
|||
ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:save`, appId, row, table) |
|||
ctx.message = `${table.name} saved successfully` |
|||
ctx.body = row |
|||
} catch (err) { |
|||
ctx.throw(400, err) |
|||
} |
|||
} |
|||
|
|||
exports.fetchView = async function (ctx) { |
|||
const tableId = getTableId(ctx) |
|||
try { |
|||
ctx.body = await pickApi(tableId).fetchView(ctx) |
|||
} catch (err) { |
|||
ctx.throw(400, err) |
|||
} |
|||
} |
|||
|
|||
exports.fetch = async function (ctx) { |
|||
const tableId = getTableId(ctx) |
|||
try { |
|||
ctx.body = await pickApi(tableId).fetch(ctx) |
|||
} catch (err) { |
|||
ctx.throw(400, err) |
|||
} |
|||
} |
|||
|
|||
exports.find = async function (ctx) { |
|||
const tableId = getTableId(ctx) |
|||
try { |
|||
ctx.body = await pickApi(tableId).find(ctx) |
|||
} catch (err) { |
|||
ctx.throw(400, err) |
|||
} |
|||
} |
|||
|
|||
exports.destroy = async function (ctx) { |
|||
const appId = ctx.appId |
|||
const inputs = ctx.request.body |
|||
const tableId = getTableId(ctx) |
|||
let response, row |
|||
if (inputs.rows) { |
|||
let { rows } = await pickApi(tableId).bulkDestroy(ctx) |
|||
response = rows |
|||
for (let row of rows) { |
|||
ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:delete`, appId, row) |
|||
} |
|||
} else { |
|||
let resp = await pickApi(tableId).destroy(ctx) |
|||
response = resp.response |
|||
row = resp.row |
|||
ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:delete`, appId, row) |
|||
} |
|||
ctx.status = 200 |
|||
// for automations include the row that was deleted
|
|||
ctx.row = row || {} |
|||
ctx.body = response |
|||
} |
|||
|
|||
exports.search = async ctx => { |
|||
const tableId = getTableId(ctx) |
|||
try { |
|||
ctx.body = await pickApi(tableId).search(ctx) |
|||
} catch (err) { |
|||
ctx.throw(400, err) |
|||
} |
|||
} |
|||
|
|||
exports.validate = async function (ctx) { |
|||
const tableId = getTableId(ctx) |
|||
try { |
|||
ctx.body = await pickApi(tableId).validate(ctx) |
|||
} catch (err) { |
|||
ctx.throw(400, err) |
|||
} |
|||
} |
|||
|
|||
exports.fetchEnrichedRow = async function (ctx) { |
|||
const tableId = getTableId(ctx) |
|||
try { |
|||
ctx.body = await pickApi(tableId).fetchEnrichedRow(ctx) |
|||
} catch (err) { |
|||
ctx.throw(400, err) |
|||
} |
|||
} |
|||
@ -0,0 +1,70 @@ |
|||
const validateJs = require("validate.js") |
|||
const { cloneDeep } = require("lodash/fp") |
|||
const CouchDB = require("../../../db") |
|||
const { InternalTables } = require("../../../db/utils") |
|||
const userController = require("../user") |
|||
const { FieldTypes } = require("../../../constants") |
|||
const { integrations } = require("../../../integrations") |
|||
|
|||
validateJs.extend(validateJs.validators.datetime, { |
|||
parse: function (value) { |
|||
return new Date(value).getTime() |
|||
}, |
|||
// Input is a unix timestamp
|
|||
format: function (value) { |
|||
return new Date(value).toISOString() |
|||
}, |
|||
}) |
|||
|
|||
exports.makeExternalQuery = async (appId, json) => { |
|||
const datasourceId = json.endpoint.datasourceId |
|||
const db = new CouchDB(appId) |
|||
const datasource = await db.get(datasourceId) |
|||
const Integration = integrations[datasource.source] |
|||
// query is the opinionated function
|
|||
if (Integration.prototype.query) { |
|||
const integration = new Integration(datasource.config) |
|||
return integration.query(json) |
|||
} else { |
|||
throw "Datasource does not support query." |
|||
} |
|||
} |
|||
|
|||
exports.findRow = async (ctx, db, tableId, rowId) => { |
|||
let row |
|||
// TODO remove special user case in future
|
|||
if (tableId === InternalTables.USER_METADATA) { |
|||
ctx.params = { |
|||
id: rowId, |
|||
} |
|||
await userController.findMetadata(ctx) |
|||
row = ctx.body |
|||
} else { |
|||
row = await db.get(rowId) |
|||
} |
|||
if (row.tableId !== tableId) { |
|||
throw "Supplied tableId does not match the rows tableId" |
|||
} |
|||
return row |
|||
} |
|||
|
|||
exports.validate = async ({ appId, tableId, row, table }) => { |
|||
if (!table) { |
|||
const db = new CouchDB(appId) |
|||
table = await db.get(tableId) |
|||
} |
|||
const errors = {} |
|||
for (let fieldName of Object.keys(table.schema)) { |
|||
const constraints = cloneDeep(table.schema[fieldName].constraints) |
|||
// special case for options, need to always allow unselected (null)
|
|||
if ( |
|||
table.schema[fieldName].type === FieldTypes.OPTIONS && |
|||
constraints.inclusion |
|||
) { |
|||
constraints.inclusion.push(null) |
|||
} |
|||
const res = validateJs.single(row[fieldName], constraints) |
|||
if (res) errors[fieldName] = res |
|||
} |
|||
return { valid: Object.keys(errors).length === 0, errors } |
|||
} |
|||
@ -1,26 +0,0 @@ |
|||
const { fullSearch, paginatedSearch } = require("./utils") |
|||
const CouchDB = require("../../../db") |
|||
const { outputProcessing } = require("../../../utilities/rowProcessor") |
|||
|
|||
exports.rowSearch = async ctx => { |
|||
const appId = ctx.appId |
|||
const { tableId } = ctx.params |
|||
const db = new CouchDB(appId) |
|||
const { paginate, query, ...params } = ctx.request.body |
|||
params.tableId = tableId |
|||
|
|||
let response |
|||
if (paginate) { |
|||
response = await paginatedSearch(appId, query, params) |
|||
} else { |
|||
response = await fullSearch(appId, query, params) |
|||
} |
|||
|
|||
// Enrich search results with relationships
|
|||
if (response.rows && response.rows.length) { |
|||
const table = await db.get(tableId) |
|||
response.rows = await outputProcessing(appId, table, response.rows) |
|||
} |
|||
|
|||
ctx.body = response |
|||
} |
|||
@ -1,19 +0,0 @@ |
|||
const Router = require("@koa/router") |
|||
const controller = require("../controllers/search") |
|||
const { |
|||
PermissionTypes, |
|||
PermissionLevels, |
|||
} = require("@budibase/auth/permissions") |
|||
const authorized = require("../../middleware/authorized") |
|||
const { paramResource } = require("../../middleware/resourceId") |
|||
|
|||
const router = Router() |
|||
|
|||
router.post( |
|||
"/api/search/:tableId/rows", |
|||
paramResource("tableId"), |
|||
authorized(PermissionTypes.TABLE, PermissionLevels.READ), |
|||
controller.rowSearch |
|||
) |
|||
|
|||
module.exports = router |
|||
@ -0,0 +1,89 @@ |
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP |
|||
|
|||
exports[`/datasources fetch returns all the datasources from the server 1`] = ` |
|||
Array [ |
|||
Object { |
|||
"config": Object {}, |
|||
"entities": Array [ |
|||
Object { |
|||
"_id": "ta_users", |
|||
"_rev": "1-039883a06c1f9cb3945731d79838181e", |
|||
"name": "Users", |
|||
"primaryDisplay": "email", |
|||
"schema": Object { |
|||
"email": Object { |
|||
"constraints": Object { |
|||
"email": true, |
|||
"length": Object { |
|||
"maximum": "", |
|||
}, |
|||
"presence": true, |
|||
"type": "string", |
|||
}, |
|||
"fieldName": "email", |
|||
"name": "email", |
|||
"type": "string", |
|||
}, |
|||
"firstName": Object { |
|||
"constraints": Object { |
|||
"presence": false, |
|||
"type": "string", |
|||
}, |
|||
"fieldName": "firstName", |
|||
"name": "firstName", |
|||
"type": "string", |
|||
}, |
|||
"lastName": Object { |
|||
"constraints": Object { |
|||
"presence": false, |
|||
"type": "string", |
|||
}, |
|||
"fieldName": "lastName", |
|||
"name": "lastName", |
|||
"type": "string", |
|||
}, |
|||
"roleId": Object { |
|||
"constraints": Object { |
|||
"inclusion": Array [ |
|||
"ADMIN", |
|||
"POWER", |
|||
"BASIC", |
|||
"PUBLIC", |
|||
], |
|||
"presence": false, |
|||
"type": "string", |
|||
}, |
|||
"fieldName": "roleId", |
|||
"name": "roleId", |
|||
"type": "options", |
|||
}, |
|||
"status": Object { |
|||
"constraints": Object { |
|||
"inclusion": Array [ |
|||
"active", |
|||
"inactive", |
|||
], |
|||
"presence": false, |
|||
"type": "string", |
|||
}, |
|||
"fieldName": "status", |
|||
"name": "status", |
|||
"type": "options", |
|||
}, |
|||
}, |
|||
"type": "table", |
|||
"views": Object {}, |
|||
}, |
|||
], |
|||
"name": "Budibase DB", |
|||
"source": "BUDIBASE", |
|||
"type": "budibase", |
|||
}, |
|||
Object { |
|||
"config": Object {}, |
|||
"name": "Test", |
|||
"source": "POSTGRES", |
|||
"type": "datasource", |
|||
}, |
|||
] |
|||
`; |
|||
@ -0,0 +1,11 @@ |
|||
exports.Operation = { |
|||
CREATE: "CREATE", |
|||
READ: "READ", |
|||
UPDATE: "UPDATE", |
|||
DELETE: "DELETE", |
|||
} |
|||
|
|||
exports.SortDirection = { |
|||
ASCENDING: "ASCENDING", |
|||
DESCENDING: "DESCENDING", |
|||
} |
|||
@ -0,0 +1,175 @@ |
|||
const { DataSourceOperation, SortDirection } = require("../../constants") |
|||
|
|||
const BASE_LIMIT = 5000 |
|||
|
|||
function addFilters(query, filters) { |
|||
function iterate(structure, fn) { |
|||
for (let [key, value] of Object.entries(structure)) { |
|||
fn(key, value) |
|||
} |
|||
} |
|||
if (!filters) { |
|||
return query |
|||
} |
|||
// if all or specified in filters, then everything is an or
|
|||
const allOr = !!filters.allOr |
|||
if (filters.string) { |
|||
iterate(filters.string, (key, value) => { |
|||
const fnc = allOr ? "orWhere" : "where" |
|||
query = query[fnc](key, "like", `${value}%`) |
|||
}) |
|||
} |
|||
if (filters.range) { |
|||
iterate(filters.range, (key, value) => { |
|||
if (!value.high || !value.low) { |
|||
return |
|||
} |
|||
const fnc = allOr ? "orWhereBetween" : "whereBetween" |
|||
query = query[fnc](key, [value.low, value.high]) |
|||
}) |
|||
} |
|||
if (filters.equal) { |
|||
iterate(filters.equal, (key, value) => { |
|||
const fnc = allOr ? "orWhere" : "where" |
|||
query = query[fnc]({ [key]: value }) |
|||
}) |
|||
} |
|||
if (filters.notEqual) { |
|||
iterate(filters.notEqual, (key, value) => { |
|||
const fnc = allOr ? "orWhereNot" : "whereNot" |
|||
query = query[fnc]({ [key]: value }) |
|||
}) |
|||
} |
|||
if (filters.empty) { |
|||
iterate(filters.empty, key => { |
|||
const fnc = allOr ? "orWhereNull" : "whereNull" |
|||
query = query[fnc](key) |
|||
}) |
|||
} |
|||
if (filters.notEmpty) { |
|||
iterate(filters.notEmpty, key => { |
|||
const fnc = allOr ? "orWhereNotNull" : "whereNotNull" |
|||
query = query[fnc](key) |
|||
}) |
|||
} |
|||
return query |
|||
} |
|||
|
|||
function buildCreate(knex, json, opts) { |
|||
const { endpoint, body } = json |
|||
let query = knex(endpoint.entityId) |
|||
// mysql can't use returning
|
|||
if (opts.disableReturning) { |
|||
return query.insert(body) |
|||
} else { |
|||
return query.insert(body).returning("*") |
|||
} |
|||
} |
|||
|
|||
function buildRead(knex, json, limit) { |
|||
let { endpoint, resource, filters, sort, paginate } = json |
|||
let query = knex(endpoint.entityId) |
|||
// select all if not specified
|
|||
if (!resource) { |
|||
resource = { fields: [] } |
|||
} |
|||
// handle select
|
|||
if (resource.fields && resource.fields.length > 0) { |
|||
query = query.select(resource.fields) |
|||
} else { |
|||
query = query.select("*") |
|||
} |
|||
// handle where
|
|||
query = addFilters(query, filters) |
|||
// handle sorting
|
|||
if (sort) { |
|||
for (let [key, value] of Object.entries(sort)) { |
|||
const direction = value === SortDirection.ASCENDING ? "asc" : "desc" |
|||
query = query.orderBy(key, direction) |
|||
} |
|||
} |
|||
// handle pagination
|
|||
if (paginate && paginate.page && paginate.limit) { |
|||
const page = paginate.page <= 1 ? 0 : paginate.page - 1 |
|||
const offset = page * paginate.limit |
|||
query = query.offset(offset).limit(paginate.limit) |
|||
} else if (paginate && paginate.limit) { |
|||
query = query.limit(paginate.limit) |
|||
} else { |
|||
query.limit(limit) |
|||
} |
|||
return query |
|||
} |
|||
|
|||
function buildUpdate(knex, json, opts) { |
|||
const { endpoint, body, filters } = json |
|||
let query = knex(endpoint.entityId) |
|||
query = addFilters(query, filters) |
|||
// mysql can't use returning
|
|||
if (opts.disableReturning) { |
|||
return query.update(body) |
|||
} else { |
|||
return query.update(body).returning("*") |
|||
} |
|||
} |
|||
|
|||
function buildDelete(knex, json, opts) { |
|||
const { endpoint, filters } = json |
|||
let query = knex(endpoint.entityId) |
|||
query = addFilters(query, filters) |
|||
// mysql can't use returning
|
|||
if (opts.disableReturning) { |
|||
return query.delete() |
|||
} else { |
|||
return query.delete().returning("*") |
|||
} |
|||
} |
|||
|
|||
class SqlQueryBuilder { |
|||
// pass through client to get flavour of SQL
|
|||
constructor(client, limit = BASE_LIMIT) { |
|||
this._client = client |
|||
this._limit = limit |
|||
} |
|||
|
|||
/** |
|||
* @param json the input JSON structure from which an SQL query will be built. |
|||
* @return {string} the operation that was found in the JSON. |
|||
*/ |
|||
_operation(json) { |
|||
if (!json || !json.endpoint) { |
|||
return "" |
|||
} |
|||
return json.endpoint.operation |
|||
} |
|||
|
|||
/** |
|||
* @param json The JSON query DSL which is to be converted to SQL. |
|||
* @param opts extra options which are to be passed into the query builder, e.g. disableReturning |
|||
* which for the sake of mySQL stops adding the returning statement to inserts, updates and deletes. |
|||
* @return {{ sql: string, bindings: object }} the query ready to be passed to the driver. |
|||
*/ |
|||
_query(json, opts = {}) { |
|||
const knex = require("knex")({ client: this._client }) |
|||
let query |
|||
switch (this._operation(json)) { |
|||
case DataSourceOperation.CREATE: |
|||
query = buildCreate(knex, json, opts) |
|||
break |
|||
case DataSourceOperation.READ: |
|||
query = buildRead(knex, json, this._limit, opts) |
|||
break |
|||
case DataSourceOperation.UPDATE: |
|||
query = buildUpdate(knex, json, opts) |
|||
break |
|||
case DataSourceOperation.DELETE: |
|||
query = buildDelete(knex, json, opts) |
|||
break |
|||
default: |
|||
throw `Operation type is not supported by SQL query builder` |
|||
} |
|||
return query.toSQL().toNative() |
|||
} |
|||
} |
|||
|
|||
module.exports = SqlQueryBuilder |
|||
@ -0,0 +1,173 @@ |
|||
const Sql = require("../base/sql") |
|||
|
|||
const TABLE_NAME = "test" |
|||
|
|||
function endpoint(table, operation) { |
|||
return { |
|||
datasourceId: "Postgres", |
|||
operation: operation, |
|||
entityId: table || TABLE_NAME, |
|||
} |
|||
} |
|||
|
|||
function generateReadJson({ table, fields, filters, sort, paginate} = {}) { |
|||
return { |
|||
endpoint: endpoint(table || TABLE_NAME, "READ"), |
|||
resource: { |
|||
fields: fields || [], |
|||
}, |
|||
filters: filters || {}, |
|||
sort: sort || {}, |
|||
paginate: paginate || {}, |
|||
} |
|||
} |
|||
|
|||
function generateCreateJson(table = TABLE_NAME, body = {}) { |
|||
return { |
|||
endpoint: endpoint(table, "CREATE"), |
|||
body, |
|||
} |
|||
} |
|||
|
|||
function generateUpdateJson(table = TABLE_NAME, body = {}, filters = {}) { |
|||
return { |
|||
endpoint: endpoint(table, "UPDATE"), |
|||
filters, |
|||
body, |
|||
} |
|||
} |
|||
|
|||
function generateDeleteJson(table = TABLE_NAME, filters = {}) { |
|||
return { |
|||
endpoint: endpoint(table, "DELETE"), |
|||
filters, |
|||
} |
|||
} |
|||
|
|||
describe("SQL query builder", () => { |
|||
const limit = 500 |
|||
const client = "pg" |
|||
let sql |
|||
|
|||
beforeEach(() => { |
|||
sql = new Sql(client, limit) |
|||
}) |
|||
|
|||
it("should test a basic read", () => { |
|||
const query = sql._query(generateReadJson()) |
|||
expect(query).toEqual({ |
|||
bindings: [limit], |
|||
sql: `select * from "${TABLE_NAME}" limit $1` |
|||
}) |
|||
}) |
|||
|
|||
it("should test a read with specific columns", () => { |
|||
const query = sql._query(generateReadJson({ |
|||
fields: ["name", "age"] |
|||
})) |
|||
expect(query).toEqual({ |
|||
bindings: [limit], |
|||
sql: `select "name", "age" from "${TABLE_NAME}" limit $1` |
|||
}) |
|||
}) |
|||
|
|||
it("should test a where string starts with read", () => { |
|||
const query = sql._query(generateReadJson({ |
|||
filters: { |
|||
string: { |
|||
name: "John", |
|||
} |
|||
} |
|||
})) |
|||
expect(query).toEqual({ |
|||
bindings: ["John%", limit], |
|||
sql: `select * from "${TABLE_NAME}" where "name" like $1 limit $2` |
|||
}) |
|||
}) |
|||
|
|||
it("should test a where range read", () => { |
|||
const query = sql._query(generateReadJson({ |
|||
filters: { |
|||
range: { |
|||
age: { |
|||
low: 2, |
|||
high: 10, |
|||
} |
|||
} |
|||
} |
|||
})) |
|||
expect(query).toEqual({ |
|||
bindings: [2, 10, limit], |
|||
sql: `select * from "${TABLE_NAME}" where "age" between $1 and $2 limit $3` |
|||
}) |
|||
}) |
|||
|
|||
it("should test for multiple IDs with OR", () => { |
|||
const query = sql._query(generateReadJson({ |
|||
filters: { |
|||
equal: { |
|||
age: 10, |
|||
name: "John", |
|||
}, |
|||
allOr: true, |
|||
} |
|||
})) |
|||
expect(query).toEqual({ |
|||
bindings: [10, "John", limit], |
|||
sql: `select * from "${TABLE_NAME}" where ("age" = $1) or ("name" = $2) limit $3` |
|||
}) |
|||
}) |
|||
|
|||
it("should test an create statement", () => { |
|||
const query = sql._query(generateCreateJson(TABLE_NAME, { |
|||
name: "Michael", |
|||
age: 45, |
|||
})) |
|||
expect(query).toEqual({ |
|||
bindings: [45, "Michael"], |
|||
sql: `insert into "${TABLE_NAME}" ("age", "name") values ($1, $2) returning *` |
|||
}) |
|||
}) |
|||
|
|||
it("should test an update statement", () => { |
|||
const query = sql._query(generateUpdateJson(TABLE_NAME, { |
|||
name: "John" |
|||
}, { |
|||
equal: { |
|||
id: 1001, |
|||
} |
|||
})) |
|||
expect(query).toEqual({ |
|||
bindings: ["John", 1001], |
|||
sql: `update "${TABLE_NAME}" set "name" = $1 where "id" = $2 returning *` |
|||
}) |
|||
}) |
|||
|
|||
it("should test a delete statement", () => { |
|||
const query = sql._query(generateDeleteJson(TABLE_NAME, { |
|||
equal: { |
|||
id: 1001, |
|||
} |
|||
})) |
|||
expect(query).toEqual({ |
|||
bindings: [1001], |
|||
sql: `delete from "${TABLE_NAME}" where "id" = $1 returning *` |
|||
}) |
|||
}) |
|||
|
|||
it("should work with MS-SQL", () => { |
|||
const query = new Sql("mssql", 10)._query(generateReadJson()) |
|||
expect(query).toEqual({ |
|||
bindings: [10], |
|||
sql: `select top (@p0) * from [${TABLE_NAME}]` |
|||
}) |
|||
}) |
|||
|
|||
it("should work with mySQL", () => { |
|||
const query = new Sql("mysql", 10)._query(generateReadJson()) |
|||
expect(query).toEqual({ |
|||
bindings: [10], |
|||
sql: `select * from \`${TABLE_NAME}\` limit ?` |
|||
}) |
|||
}) |
|||
}) |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue