Merge branch 'develop' into google_firebase_integration
This commit is contained in:
commit
19ec76c49d
|
@ -42,7 +42,7 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
docker login -u $DOCKER_USER -p $DOCKER_PASSWORD
|
docker login -u $DOCKER_USER -p $DOCKER_PASSWORD
|
||||||
yarn build:docker:proxy:prod
|
yarn build:docker:proxy:prod
|
||||||
docker tag budibase/proxy:$release_tag budibase/proxy:$PROD_TAG
|
docker tag proxy-service budibase/proxy:$PROD_TAG
|
||||||
docker push budibase/proxy:$PROD_TAG
|
docker push budibase/proxy:$PROD_TAG
|
||||||
env:
|
env:
|
||||||
DOCKER_USER: ${{ secrets.DOCKER_USERNAME }}
|
DOCKER_USER: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
|
|
@ -34,7 +34,7 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
docker login -u $DOCKER_USER -p $DOCKER_PASSWORD
|
docker login -u $DOCKER_USER -p $DOCKER_PASSWORD
|
||||||
yarn build:docker:proxy:preprod
|
yarn build:docker:proxy:preprod
|
||||||
docker tag budibase/proxy:$release_tag budibase/proxy:$PREPROD_TAG
|
docker tag proxy-service budibase/proxy:$PREPROD_TAG
|
||||||
docker push budibase/proxy:$PREPROD_TAG
|
docker push budibase/proxy:$PREPROD_TAG
|
||||||
env:
|
env:
|
||||||
DOCKER_USER: ${{ secrets.DOCKER_USERNAME }}
|
DOCKER_USER: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
|
|
@ -48,7 +48,7 @@ jobs:
|
||||||
yarn build
|
yarn build
|
||||||
popd
|
popd
|
||||||
|
|
||||||
- name: Build OpenAPI sepc
|
- name: Build OpenAPI spec
|
||||||
run: |
|
run: |
|
||||||
pushd packages/server
|
pushd packages/server
|
||||||
yarn
|
yarn
|
||||||
|
@ -63,6 +63,7 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
git config user.name "Budibase Helm Bot"
|
git config user.name "Budibase Helm Bot"
|
||||||
git config user.email "<>"
|
git config user.email "<>"
|
||||||
|
git reset --hard
|
||||||
git pull
|
git pull
|
||||||
helm package charts/budibase
|
helm package charts/budibase
|
||||||
git checkout gh-pages
|
git checkout gh-pages
|
||||||
|
|
|
@ -98,3 +98,4 @@ hosting/proxy/.generated-nginx.prod.conf
|
||||||
bin/
|
bin/
|
||||||
hosting/.generated*
|
hosting/.generated*
|
||||||
packages/builder/cypress.env.json
|
packages/builder/cypress.env.json
|
||||||
|
stats.html
|
||||||
|
|
|
@ -5,6 +5,7 @@ packages/builder/src/components/design/AppPreview/CurrentItemPreview.svelte
|
||||||
packages/server/builder
|
packages/server/builder
|
||||||
packages/server/coverage
|
packages/server/coverage
|
||||||
packages/server/client
|
packages/server/client
|
||||||
|
packages/server/src/definitions/openapi.ts
|
||||||
packages/builder/.routify
|
packages/builder/.routify
|
||||||
packages/builder/cypress/support/queryLevelTransformerFunction.js
|
packages/builder/cypress/support/queryLevelTransformerFunction.js
|
||||||
packages/builder/cypress/support/queryLevelTransformerFunctionWithData.js
|
packages/builder/cypress/support/queryLevelTransformerFunctionWithData.js
|
||||||
|
|
14
README.md
14
README.md
|
@ -11,7 +11,7 @@
|
||||||
The low code platform you'll enjoy using
|
The low code platform you'll enjoy using
|
||||||
</h3>
|
</h3>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
Budibase is an open source low-code platform, and the easiest way to build internal tools that improve productivity.
|
Budibase is an open source low-code platform, and the easiest way to build internal apps that improve productivity.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h3 align="center">
|
<h3 align="center">
|
||||||
|
@ -40,9 +40,11 @@
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h3 align="center">
|
<h3 align="center">
|
||||||
<a href="https://docs.budibase.com/getting-started">Get started</a>
|
<a href="https://account.budibase.app/register">Get started - we host (Budibase Cloud)</a>
|
||||||
<span> · </span>
|
<span> · </span>
|
||||||
<a href="https://docs.budibase.com">Docs</a>
|
<a href="https://docs.budibase.com/docs/hosting-methods">Get started - you host (Docker, K8s, DO)</a>
|
||||||
|
<span> · </span>
|
||||||
|
<a href="https://docs.budibase.com/docs">Docs</a>
|
||||||
<span> · </span>
|
<span> · </span>
|
||||||
<a href="https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas">Feature request</a>
|
<a href="https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas">Feature request</a>
|
||||||
<span> · </span>
|
<span> · </span>
|
||||||
|
@ -104,12 +106,12 @@ Budibase is made to scale. With Budibase, you can self-host on your own infrastr
|
||||||
|
|
||||||
## 🏁 Get started
|
## 🏁 Get started
|
||||||
|
|
||||||
<a href="https://docs.budibase.com/self-hosting/self-host"><img src="https://res.cloudinary.com/daog6scxm/image/upload/v1634808888/logo/deploy_npl9za.png" /></a>
|
<a href="https://docs.budibase.com/docs/hosting-methods"><img src="https://res.cloudinary.com/daog6scxm/image/upload/v1634808888/logo/deploy_npl9za.png" /></a>
|
||||||
|
|
||||||
Deploy Budibase self-hosted in your existing infrastructure, using Docker, Kubernetes, and Digital Ocean.
|
Deploy Budibase self-hosted in your existing infrastructure, using Docker, Kubernetes, and Digital Ocean.
|
||||||
Or use Budibase Cloud if you don't need to self-host, and would like to get started quickly.
|
Or use Budibase Cloud if you don't need to self-host, and would like to get started quickly.
|
||||||
|
|
||||||
### [Get started with self-hosting Budibase](https://docs.budibase.com/self-hosting/self-host)
|
### [Get started with self-hosting Budibase](https://docs.budibase.com/docs/hosting-methods)
|
||||||
|
|
||||||
### [Get started with Budibase Cloud](https://budibase.com)
|
### [Get started with Budibase Cloud](https://budibase.com)
|
||||||
|
|
||||||
|
@ -118,7 +120,7 @@ Or use Budibase Cloud if you don't need to self-host, and would like to get star
|
||||||
|
|
||||||
## 🎓 Learning Budibase
|
## 🎓 Learning Budibase
|
||||||
|
|
||||||
The Budibase documentation [lives here](https://docs.budibase.com).
|
The Budibase documentation [lives here](https://docs.budibase.com/docs).
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"extends": "next/core-web-vitals"
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
|
@ -0,0 +1,41 @@
|
||||||
|
# Budibase API + Next.js example
|
||||||
|
|
||||||
|
This is an example of how Budibase can be used as a backend for a Postgres database for a Next.js sales app. You will
|
||||||
|
need to follow the walk-through that has been published in the Budibase docs to set up your Budibase app for this example.
|
||||||
|
|
||||||
|
## Pre-requisites
|
||||||
|
|
||||||
|
To use this example you will need:
|
||||||
|
1. [Docker](https://www.docker.com/)
|
||||||
|
2. [Docker Compose](https://docs.docker.com/compose/)
|
||||||
|
3. [Node.js](https://nodejs.org/en/)
|
||||||
|
4. A self-hosted Budibase installation
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
The first step is to set up the database - you can do this by going to the `db/` directory and running the command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose up
|
||||||
|
```
|
||||||
|
|
||||||
|
The next step is to follow the example walk-through and set up a Budibase app as it describes. Once you've done
|
||||||
|
this you can configure the settings in `next.config.js`, specifically the `apiKey`, `host` and `appName`.
|
||||||
|
|
||||||
|
Finally, you can start the dev server with the following command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
# or
|
||||||
|
yarn dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Accessing the app
|
||||||
|
|
||||||
|
Open [http://localhost:3001](http://localhost:3001) with your browser to see the sales app.
|
||||||
|
|
||||||
|
Look in the API routes (`pages/api/sales.ts` and `pages/api/salespeople.ts`) to see how this is integrated with Budibase.
|
||||||
|
There is also a utility file where some core functions and types have been defined, in `utilities/index.ts`.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
This example was set up using [Next.js](https://nextjs.org/) and bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
|
|
@ -0,0 +1,42 @@
|
||||||
|
import Link from "next/link"
|
||||||
|
import Image from "next/image"
|
||||||
|
import { ReactNotifications } from "react-notifications-component"
|
||||||
|
|
||||||
|
function layout(props: any) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<nav className="navbar" role="navigation" aria-label="main navigation">
|
||||||
|
<div id="navbar" className="navbar-menu">
|
||||||
|
<div className="logo">
|
||||||
|
<Image alt="logo" src="/bb-emblem.svg" width="50" height="50" />
|
||||||
|
</div>
|
||||||
|
<div className="navbar-start">
|
||||||
|
<Link href="/">
|
||||||
|
<a className="navbar-item">
|
||||||
|
List
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
<Link href="/save">
|
||||||
|
<a className="navbar-item">
|
||||||
|
New sale
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div className="navbar-end">
|
||||||
|
<div className="navbar-item">
|
||||||
|
<div className="buttons">
|
||||||
|
<a className="button is-primary" href="https://budibase.readme.io/reference">
|
||||||
|
<strong>API Documentation</strong>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<ReactNotifications />
|
||||||
|
{props.children}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default layout
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { Store } from "react-notifications-component"
|
||||||
|
|
||||||
|
const notifications = {
|
||||||
|
error: (error: string, title: string) => {
|
||||||
|
Store.addNotification({
|
||||||
|
container: "top-right",
|
||||||
|
type: "danger",
|
||||||
|
message: error,
|
||||||
|
title: title,
|
||||||
|
dismiss: {
|
||||||
|
duration: 10000,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
success: (message: string, title: string) => {
|
||||||
|
Store.addNotification({
|
||||||
|
container: "top-right",
|
||||||
|
type: "success",
|
||||||
|
message: message,
|
||||||
|
title: title,
|
||||||
|
dismiss: {
|
||||||
|
duration: 3000,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default notifications
|
|
@ -0,0 +1,17 @@
|
||||||
|
version: "3.8"
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
container_name: postgres
|
||||||
|
image: postgres
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: root
|
||||||
|
POSTGRES_PASSWORD: root
|
||||||
|
POSTGRES_DB: postgres
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
volumes:
|
||||||
|
- pg_data:/var/lib/postgresql/data/
|
||||||
|
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
|
||||||
|
volumes:
|
||||||
|
pg_data:
|
|
@ -0,0 +1,21 @@
|
||||||
|
CREATE TABLE IF NOT EXISTS sales_people (
|
||||||
|
person_id SERIAL PRIMARY KEY,
|
||||||
|
name varchar(200) NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS sales (
|
||||||
|
sale_id SERIAL PRIMARY KEY,
|
||||||
|
sale_name varchar(200) NOT NULL,
|
||||||
|
sold_by INT,
|
||||||
|
CONSTRAINT sold_by_fk
|
||||||
|
FOREIGN KEY(sold_by)
|
||||||
|
REFERENCES sales_people(person_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO sales_people (name)
|
||||||
|
select 'Salesperson ' || id
|
||||||
|
FROM GENERATE_SERIES(1, 50) as id;
|
||||||
|
|
||||||
|
INSERT INTO sales (sale_name, sold_by)
|
||||||
|
select 'Sale ' || id, floor(random() * 50 + 1)::int
|
||||||
|
FROM GENERATE_SERIES(1, 200) as id;
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { components } from "./openapi"
|
||||||
|
|
||||||
|
export type App = components["schemas"]["applicationOutput"]["data"]
|
||||||
|
export type Table = components["schemas"]["tableOutput"]["data"]
|
||||||
|
export type TableSearch = components["schemas"]["tableSearch"]
|
||||||
|
export type AppSearch = components["schemas"]["applicationSearch"]
|
||||||
|
export type RowSearch = components["schemas"]["searchOutput"]
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,5 @@
|
||||||
|
/// <reference types="next" />
|
||||||
|
/// <reference types="next/image-types/global" />
|
||||||
|
|
||||||
|
// NOTE: This file should not be edited
|
||||||
|
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
|
@ -0,0 +1,16 @@
|
||||||
|
const { join } = require("path")
|
||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
|
reactStrictMode: true,
|
||||||
|
sassOptions: {
|
||||||
|
includePaths: [join(__dirname, "styles")],
|
||||||
|
},
|
||||||
|
serverRuntimeConfig: {
|
||||||
|
apiKey:
|
||||||
|
"bf4d86af933b5ac0af0fdbe4bf7d89ff-f929752a1eeaafb00f4b5e3325097d51a44fe4b39f22ed857923409cc75414b379323a25ebfb4916",
|
||||||
|
appName: "sales",
|
||||||
|
host: "http://localhost:10000",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = nextConfig
|
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"name": "nextjs-api-sales",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev -p 3001",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"bulma": "^0.9.3",
|
||||||
|
"next": "12.1.0",
|
||||||
|
"node-fetch": "^3.2.2",
|
||||||
|
"node-sass": "^7.0.1",
|
||||||
|
"react": "17.0.2",
|
||||||
|
"react-dom": "17.0.2",
|
||||||
|
"react-notifications-component": "^3.4.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "17.0.21",
|
||||||
|
"@types/react": "17.0.39",
|
||||||
|
"eslint": "8.10.0",
|
||||||
|
"eslint-config-next": "12.1.0",
|
||||||
|
"typescript": "4.6.2"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
import "../styles/global.sass"
|
||||||
|
import type { AppProps } from "next/app"
|
||||||
|
import Head from "next/head"
|
||||||
|
import Layout from "../components/layout"
|
||||||
|
|
||||||
|
function MyApp({ Component, pageProps }: AppProps) {
|
||||||
|
return (
|
||||||
|
<Layout>
|
||||||
|
<Head>
|
||||||
|
<title>BB NextJS Sales Example</title>
|
||||||
|
</Head>
|
||||||
|
<Component {...pageProps} />
|
||||||
|
</Layout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MyApp
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { getApp, findTable, makeCall } from "../../utilities"
|
||||||
|
|
||||||
|
async function getSales(req: any) {
|
||||||
|
const { page } = req.query
|
||||||
|
const { _id: appId } = await getApp()
|
||||||
|
const table = await findTable(appId, "sales")
|
||||||
|
return await makeCall("post", `tables/${table._id}/rows/search`, {
|
||||||
|
appId,
|
||||||
|
body: {
|
||||||
|
limit: 10,
|
||||||
|
sort: {
|
||||||
|
type: "string",
|
||||||
|
order: "descending",
|
||||||
|
column: "sale_id",
|
||||||
|
},
|
||||||
|
paginate: true,
|
||||||
|
bookmark: parseInt(page),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveSale(req: any) {
|
||||||
|
const { _id: appId } = await getApp()
|
||||||
|
const table = await findTable(appId, "sales")
|
||||||
|
return await makeCall("post", `tables/${table._id}/rows`, {
|
||||||
|
body: req.body,
|
||||||
|
appId,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function handler(req: any, res: any) {
|
||||||
|
let response: any = {}
|
||||||
|
try {
|
||||||
|
if (req.method === "POST") {
|
||||||
|
response = await saveSale(req)
|
||||||
|
} else if (req.method === "GET") {
|
||||||
|
response = await getSales(req)
|
||||||
|
} else {
|
||||||
|
res.status(404)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
res.status(200).json(response)
|
||||||
|
} catch (err: any) {
|
||||||
|
res.status(400).send(err)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { getApp, findTable, makeCall } from "../../utilities"
|
||||||
|
|
||||||
|
async function getSalespeople() {
|
||||||
|
const { _id: appId } = await getApp()
|
||||||
|
const table = await findTable(appId, "sales_people")
|
||||||
|
return await makeCall("post", `tables/${table._id}/rows/search`, {
|
||||||
|
appId,
|
||||||
|
body: {
|
||||||
|
sort: {
|
||||||
|
type: "string",
|
||||||
|
order: "ascending",
|
||||||
|
column: "person_id",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function handler(req: any, res: any) {
|
||||||
|
let response: any = {}
|
||||||
|
try {
|
||||||
|
if (req.method === "GET") {
|
||||||
|
response = await getSalespeople()
|
||||||
|
} else {
|
||||||
|
res.status(404)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
res.status(200).json(response)
|
||||||
|
} catch (err: any) {
|
||||||
|
res.status(400).send(err)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,83 @@
|
||||||
|
import type { NextPage } from "next"
|
||||||
|
import styles from "../styles/home.module.css"
|
||||||
|
import { useState, useEffect, useCallback } from "react"
|
||||||
|
import Notifications from "../components/notifications"
|
||||||
|
|
||||||
|
const Home: NextPage = () => {
|
||||||
|
const [sales, setSales] = useState([])
|
||||||
|
const [currentPage, setCurrentPage] = useState(1)
|
||||||
|
const [loaded, setLoaded] = useState(false)
|
||||||
|
|
||||||
|
const getSales = useCallback(async (page: Number = 1) => {
|
||||||
|
let url = "/api/sales"
|
||||||
|
if (page) {
|
||||||
|
url += `?page=${page}`
|
||||||
|
}
|
||||||
|
const response = await fetch(url)
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.text()
|
||||||
|
Notifications.error(error, "Failed to get sales")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const sales = await response.json()
|
||||||
|
// @ts-ignore
|
||||||
|
setCurrentPage(page)
|
||||||
|
return setSales(sales.data)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const goToNextPage = useCallback(async () => {
|
||||||
|
await getSales(currentPage + 1)
|
||||||
|
}, [currentPage, getSales])
|
||||||
|
|
||||||
|
const goToPrevPage = useCallback(async () => {
|
||||||
|
if (currentPage > 1) {
|
||||||
|
await getSales(currentPage - 1)
|
||||||
|
}
|
||||||
|
}, [currentPage, getSales])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getSales().then(() => {
|
||||||
|
setLoaded(true)
|
||||||
|
}).catch(() => {
|
||||||
|
setSales([])
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
if (!loaded) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={styles.tableSection}>
|
||||||
|
<h1 className="subtitle">Sales</h1>
|
||||||
|
<div className={styles.table}>
|
||||||
|
<table className="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Sale ID</th>
|
||||||
|
<th>name</th>
|
||||||
|
<th>Sold by</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{sales.map((sale: any) =>
|
||||||
|
<tr key={sale.sale_id}>
|
||||||
|
<th>{sale.sale_id}</th>
|
||||||
|
<th>{sale.sale_name}</th>
|
||||||
|
<th>{sale.sales_person?.map((person: any) => person.primaryDisplay)[0]}</th>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div className={styles.buttons}>
|
||||||
|
<button className="button" onClick={goToPrevPage}>Prev Page</button>
|
||||||
|
<button className="button" onClick={goToNextPage}>Next Page</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Home
|
|
@ -0,0 +1,81 @@
|
||||||
|
import type { NextPage } from "next"
|
||||||
|
import { useCallback, useEffect, useState } from "react"
|
||||||
|
import styles from "../styles/save.module.css"
|
||||||
|
import Notifications from "../components/notifications"
|
||||||
|
|
||||||
|
const Save: NextPage = () => {
|
||||||
|
const [salespeople, setSalespeople] = useState([])
|
||||||
|
const [loaded, setLoaded] = useState(false)
|
||||||
|
|
||||||
|
const saveSale = useCallback(async (event: any) => {
|
||||||
|
event.preventDefault()
|
||||||
|
const sale = {
|
||||||
|
sale_name: event.target.name.value,
|
||||||
|
sales_person: [event.target.soldBy.value],
|
||||||
|
}
|
||||||
|
const response = await fetch("/api/sales", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(sale),
|
||||||
|
})
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.text()
|
||||||
|
Notifications.error(error, "Failed to save sale")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Notifications.success("Sale saved successfully!", "Sale saved")
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const getSalespeople = useCallback(async () => {
|
||||||
|
const response: any = await fetch("/api/salespeople")
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(await response.text())
|
||||||
|
}
|
||||||
|
const json = await response.json()
|
||||||
|
setSalespeople(json.data)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getSalespeople().then(() => {
|
||||||
|
setLoaded(true)
|
||||||
|
}).catch(() => {
|
||||||
|
setSalespeople([])
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
if (!loaded) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={styles.formSection}>
|
||||||
|
<h1 className="subtitle">New sale</h1>
|
||||||
|
<form onSubmit={saveSale}>
|
||||||
|
<div className="field">
|
||||||
|
<label className="label">Name</label>
|
||||||
|
<div className="control">
|
||||||
|
<input id="name" className="input" type="text" placeholder="Text input" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="field">
|
||||||
|
<label className="label">Sold by</label>
|
||||||
|
<div className="control">
|
||||||
|
<div className="select">
|
||||||
|
<select id="soldBy">
|
||||||
|
{salespeople.map((person: any) => <option key={person._id} value={person._id}>{person.name}</option>)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="control">
|
||||||
|
<button className="button is-link">Submit</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Save
|
|
@ -0,0 +1,80 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 24.1.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<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">
|
||||||
|
<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>
|
After Width: | Height: | Size: 6.1 KiB |
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
|
@ -0,0 +1,26 @@
|
||||||
|
@charset "utf-8"
|
||||||
|
|
||||||
|
@import url('https://fonts.googleapis.com/css?family=Roboto:400,700')
|
||||||
|
$family-sans-serif: "Roboto", sans-serif
|
||||||
|
|
||||||
|
#__next
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
justify-content: flex-start
|
||||||
|
align-items: stretch
|
||||||
|
height: 100vh
|
||||||
|
--bg-color: #f5f5f5
|
||||||
|
|
||||||
|
.logo
|
||||||
|
padding: 0.75rem
|
||||||
|
|
||||||
|
@import "../node_modules/bulma/bulma.sass"
|
||||||
|
@import "../node_modules/react-notifications-component/dist/theme.css"
|
||||||
|
|
||||||
|
// applied after bulma styles are enabled
|
||||||
|
html
|
||||||
|
overflow-y: auto
|
||||||
|
|
||||||
|
.navbar
|
||||||
|
background-color: var(--bg-color)
|
||||||
|
color: white
|
|
@ -0,0 +1,30 @@
|
||||||
|
.container {
|
||||||
|
width: 100vw;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 5rem 2rem 0;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableSection {
|
||||||
|
padding: 2rem;
|
||||||
|
background: var(--bg-color);
|
||||||
|
width: 800px;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table table {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableSection h1 {
|
||||||
|
text-align: center;
|
||||||
|
color: black;
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
.container {
|
||||||
|
width: 100vw;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 5rem 2rem 0;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formSection {
|
||||||
|
padding: 2rem;
|
||||||
|
background: var(--bg-color);
|
||||||
|
width: 400px;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formSection h1 {
|
||||||
|
text-align: center;
|
||||||
|
color: black;
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es5",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"incremental": true
|
||||||
|
},
|
||||||
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
import { App, AppSearch, Table, TableSearch } from "../definitions"
|
||||||
|
import getConfig from "next/config"
|
||||||
|
|
||||||
|
const { serverRuntimeConfig } = getConfig()
|
||||||
|
const apiKey = serverRuntimeConfig["apiKey"]
|
||||||
|
const appName = serverRuntimeConfig["appName"]
|
||||||
|
const host = serverRuntimeConfig["host"]
|
||||||
|
|
||||||
|
let APP: App | null = null
|
||||||
|
let TABLES: { [key: string]: Table } = {}
|
||||||
|
|
||||||
|
export async function makeCall(
|
||||||
|
method: string,
|
||||||
|
url: string,
|
||||||
|
opts?: { body?: any; appId?: string }
|
||||||
|
): Promise<any> {
|
||||||
|
const fetchOpts: any = {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
"x-budibase-api-key": apiKey,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if (opts?.appId) {
|
||||||
|
fetchOpts.headers["x-budibase-app-id"] = opts.appId
|
||||||
|
}
|
||||||
|
if (opts?.body) {
|
||||||
|
fetchOpts.body =
|
||||||
|
typeof opts.body !== "string" ? JSON.stringify(opts.body) : opts.body
|
||||||
|
fetchOpts.headers["Content-Type"] = "application/json"
|
||||||
|
}
|
||||||
|
const finalUrl = `${host}/api/public/v1/${url}`
|
||||||
|
const response = await fetch(finalUrl, fetchOpts)
|
||||||
|
if (response.ok) {
|
||||||
|
return response.json()
|
||||||
|
} else {
|
||||||
|
const error = await response.text()
|
||||||
|
console.error("Budibase server error - ", error)
|
||||||
|
throw new Error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getApp(): Promise<App> {
|
||||||
|
if (APP) {
|
||||||
|
return APP
|
||||||
|
}
|
||||||
|
const apps: AppSearch = await makeCall("post", "applications/search", {
|
||||||
|
body: {
|
||||||
|
name: appName,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const app = apps.data.find((app: App) => app.name === appName)
|
||||||
|
if (!app) {
|
||||||
|
throw new Error(
|
||||||
|
"Could not find app, please make sure app name in config is correct."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
APP = app
|
||||||
|
return app
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function findTable(
|
||||||
|
appId: string,
|
||||||
|
tableName: string
|
||||||
|
): Promise<Table> {
|
||||||
|
if (TABLES[tableName]) {
|
||||||
|
return TABLES[tableName]
|
||||||
|
}
|
||||||
|
const tables: TableSearch = await makeCall("post", "tables/search", {
|
||||||
|
body: {
|
||||||
|
name: tableName,
|
||||||
|
},
|
||||||
|
appId,
|
||||||
|
})
|
||||||
|
const table = tables.data.find((table: Table) => table.name === tableName)
|
||||||
|
if (!table) {
|
||||||
|
throw new Error(
|
||||||
|
"Could not find table, please make sure your app is configured with the Postgres datasource correctly."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
TABLES[tableName] = table
|
||||||
|
return table
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "1.0.80-alpha.3",
|
"version": "1.0.91-alpha.0",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|
|
@ -39,7 +39,7 @@
|
||||||
"lint:prettier": "prettier --check \"packages/**/*.{js,ts,svelte}\"",
|
"lint:prettier": "prettier --check \"packages/**/*.{js,ts,svelte}\"",
|
||||||
"lint": "yarn run lint:eslint && yarn run lint:prettier",
|
"lint": "yarn run lint:eslint && yarn run lint:prettier",
|
||||||
"lint:fix:eslint": "eslint --fix packages",
|
"lint:fix:eslint": "eslint --fix packages",
|
||||||
"lint:fix:prettier": "prettier --write \"packages/**/*.{js,ts,svelte}\"",
|
"lint:fix:prettier": "prettier --write \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\"",
|
||||||
"lint:fix:ts": "lerna run lint:fix",
|
"lint:fix:ts": "lerna run lint:fix",
|
||||||
"lint:fix": "yarn run lint:fix:ts && yarn run lint:fix:prettier && yarn run lint:fix:eslint",
|
"lint:fix": "yarn run lint:fix:ts && yarn run lint:fix:prettier && yarn run lint:fix:eslint",
|
||||||
"test:e2e": "lerna run cy:test --stream",
|
"test:e2e": "lerna run cy:test --stream",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/backend-core",
|
"name": "@budibase/backend-core",
|
||||||
"version": "1.0.80-alpha.3",
|
"version": "1.0.91-alpha.0",
|
||||||
"description": "Budibase backend core libraries used in server and worker",
|
"description": "Budibase backend core libraries used in server and worker",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"author": "Budibase",
|
"author": "Budibase",
|
||||||
|
|
|
@ -22,3 +22,18 @@ exports.getAccount = async email => {
|
||||||
|
|
||||||
return json[0]
|
return json[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.getStatus = async () => {
|
||||||
|
const response = await api.get(`/api/status`, {
|
||||||
|
headers: {
|
||||||
|
[Headers.API_KEY]: env.ACCOUNT_PORTAL_API_KEY,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const json = await response.json()
|
||||||
|
|
||||||
|
if (response.status !== 200) {
|
||||||
|
throw new Error(`Error getting status`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return json
|
||||||
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ const { getSession, updateSessionTTL } = require("../security/sessions")
|
||||||
const { buildMatcherRegex, matches } = require("./matchers")
|
const { buildMatcherRegex, matches } = require("./matchers")
|
||||||
const env = require("../environment")
|
const env = require("../environment")
|
||||||
const { SEPARATOR, ViewNames, queryGlobalView } = require("../../db")
|
const { SEPARATOR, ViewNames, queryGlobalView } = require("../../db")
|
||||||
const { getGlobalDB } = require("../tenancy")
|
const { getGlobalDB, doInTenant } = require("../tenancy")
|
||||||
const { decrypt } = require("../security/encryption")
|
const { decrypt } = require("../security/encryption")
|
||||||
|
|
||||||
function finalise(
|
function finalise(
|
||||||
|
@ -25,7 +25,8 @@ async function checkApiKey(apiKey, populateUser) {
|
||||||
}
|
}
|
||||||
const decrypted = decrypt(apiKey)
|
const decrypted = decrypt(apiKey)
|
||||||
const tenantId = decrypted.split(SEPARATOR)[0]
|
const tenantId = decrypted.split(SEPARATOR)[0]
|
||||||
const db = getGlobalDB(tenantId)
|
return doInTenant(tenantId, async () => {
|
||||||
|
const db = getGlobalDB()
|
||||||
// api key is encrypted in the database
|
// api key is encrypted in the database
|
||||||
const userId = await queryGlobalView(
|
const userId = await queryGlobalView(
|
||||||
ViewNames.BY_API_KEY,
|
ViewNames.BY_API_KEY,
|
||||||
|
@ -35,10 +36,14 @@ async function checkApiKey(apiKey, populateUser) {
|
||||||
db
|
db
|
||||||
)
|
)
|
||||||
if (userId) {
|
if (userId) {
|
||||||
return { valid: true, user: await getUser(userId, tenantId, populateUser) }
|
return {
|
||||||
|
valid: true,
|
||||||
|
user: await getUser(userId, tenantId, populateUser),
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
throw "Invalid API key"
|
throw "Invalid API key"
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -22,12 +22,25 @@ exports.Databases = {
|
||||||
exports.SEPARATOR = SEPARATOR
|
exports.SEPARATOR = SEPARATOR
|
||||||
|
|
||||||
exports.getRedisOptions = (clustered = false) => {
|
exports.getRedisOptions = (clustered = false) => {
|
||||||
const [host, port, ...rest] = REDIS_URL.split(":")
|
let password = REDIS_PASSWORD
|
||||||
|
let url = REDIS_URL.split("//")
|
||||||
|
// get rid of the protocol
|
||||||
|
url = url.length > 1 ? url[1] : url[0]
|
||||||
|
// check for a password etc
|
||||||
|
url = url.split("@")
|
||||||
|
if (url.length > 1) {
|
||||||
|
// get the password
|
||||||
|
password = url[0].split(":")[1]
|
||||||
|
url = url[1]
|
||||||
|
} else {
|
||||||
|
url = url[0]
|
||||||
|
}
|
||||||
|
const [host, port] = url.split(":")
|
||||||
|
|
||||||
let redisProtocolUrl
|
let redisProtocolUrl
|
||||||
|
|
||||||
// fully qualified redis URL
|
// fully qualified redis URL
|
||||||
if (rest.length && /rediss?/.test(host)) {
|
if (/rediss?:\/\//.test(REDIS_URL)) {
|
||||||
redisProtocolUrl = REDIS_URL
|
redisProtocolUrl = REDIS_URL
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,13 +50,13 @@ exports.getRedisOptions = (clustered = false) => {
|
||||||
if (clustered) {
|
if (clustered) {
|
||||||
opts.redisOptions = {}
|
opts.redisOptions = {}
|
||||||
opts.redisOptions.tls = {}
|
opts.redisOptions.tls = {}
|
||||||
opts.redisOptions.password = REDIS_PASSWORD
|
opts.redisOptions.password = password
|
||||||
opts.slotsRefreshTimeout = SLOT_REFRESH_MS
|
opts.slotsRefreshTimeout = SLOT_REFRESH_MS
|
||||||
opts.dnsLookup = (address, callback) => callback(null, address)
|
opts.dnsLookup = (address, callback) => callback(null, address)
|
||||||
} else {
|
} else {
|
||||||
opts.host = host
|
opts.host = host
|
||||||
opts.port = port
|
opts.port = port
|
||||||
opts.password = REDIS_PASSWORD
|
opts.password = password
|
||||||
}
|
}
|
||||||
return { opts, host, port, redisProtocolUrl }
|
return { opts, host, port, redisProtocolUrl }
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/bbui",
|
"name": "@budibase/bbui",
|
||||||
"description": "A UI solution used in the different Budibase projects.",
|
"description": "A UI solution used in the different Budibase projects.",
|
||||||
"version": "1.0.80-alpha.3",
|
"version": "1.0.91-alpha.0",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"svelte": "src/index.js",
|
"svelte": "src/index.js",
|
||||||
"module": "dist/bbui.es.js",
|
"module": "dist/bbui.es.js",
|
||||||
|
@ -38,7 +38,7 @@
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@adobe/spectrum-css-workflow-icons": "^1.2.1",
|
"@adobe/spectrum-css-workflow-icons": "^1.2.1",
|
||||||
"@budibase/string-templates": "^1.0.80-alpha.3",
|
"@budibase/string-templates": "^1.0.91-alpha.0",
|
||||||
"@spectrum-css/actionbutton": "^1.0.1",
|
"@spectrum-css/actionbutton": "^1.0.1",
|
||||||
"@spectrum-css/actiongroup": "^1.0.1",
|
"@spectrum-css/actiongroup": "^1.0.1",
|
||||||
"@spectrum-css/avatar": "^3.0.2",
|
"@spectrum-css/avatar": "^3.0.2",
|
||||||
|
|
|
@ -57,3 +57,10 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.spectrum-Toast {
|
||||||
|
pointer-events: all;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
<script>
|
||||||
|
import "@spectrum-css/toast/dist/index-vars.css"
|
||||||
|
import Portal from "svelte-portal"
|
||||||
|
import { banner } from "../Stores/banner"
|
||||||
|
import Banner from "./Banner.svelte"
|
||||||
|
import { fly } from "svelte/transition"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Portal target=".banner-container">
|
||||||
|
<div class="banner">
|
||||||
|
{#if $banner.message}
|
||||||
|
<div transition:fly={{ y: -30 }}>
|
||||||
|
<Banner
|
||||||
|
type={$banner.type}
|
||||||
|
extraButtonText={$banner.extraButtonText}
|
||||||
|
extraButtonAction={$banner.extraButtonAction}
|
||||||
|
on:change={$banner.onChange}
|
||||||
|
>
|
||||||
|
{$banner.message}
|
||||||
|
</Banner>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</Portal>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.banner {
|
||||||
|
pointer-events: none;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { writable } from "svelte/store"
|
||||||
|
|
||||||
|
export function createBannerStore() {
|
||||||
|
const DEFAULT_CONFIG = {}
|
||||||
|
|
||||||
|
const banner = writable(DEFAULT_CONFIG)
|
||||||
|
|
||||||
|
const show = async (
|
||||||
|
// eslint-disable-next-line
|
||||||
|
config = { message, type, extraButtonText, extraButtonAction, onChange }
|
||||||
|
) => {
|
||||||
|
banner.update(store => {
|
||||||
|
return {
|
||||||
|
...store,
|
||||||
|
...config,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const showStatus = async () => {
|
||||||
|
const config = {
|
||||||
|
message: "Some systems are experiencing issues",
|
||||||
|
type: "negative",
|
||||||
|
extraButtonText: "View Status",
|
||||||
|
extraButtonAction: () => window.open("https://status.budibase.com/"),
|
||||||
|
}
|
||||||
|
|
||||||
|
await show(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe: banner.subscribe,
|
||||||
|
showStatus,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const banner = createBannerStore()
|
|
@ -60,7 +60,7 @@ export const createNotificationStore = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
function id() {
|
function id() {
|
||||||
return "_" + Math.random().toString(36).substr(2, 9)
|
return "_" + Math.random().toString(36).slice(2, 9)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const notifications = createNotificationStore()
|
export const notifications = createNotificationStore()
|
||||||
|
|
|
@ -68,7 +68,7 @@
|
||||||
})
|
})
|
||||||
|
|
||||||
function id() {
|
function id() {
|
||||||
return "_" + Math.random().toString(36).substr(2, 9)
|
return "_" + Math.random().toString(36).slice(2, 9)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -60,6 +60,7 @@ export { default as StatusLight } from "./StatusLight/StatusLight.svelte"
|
||||||
export { default as ColorPicker } from "./ColorPicker/ColorPicker.svelte"
|
export { default as ColorPicker } from "./ColorPicker/ColorPicker.svelte"
|
||||||
export { default as InlineAlert } from "./InlineAlert/InlineAlert.svelte"
|
export { default as InlineAlert } from "./InlineAlert/InlineAlert.svelte"
|
||||||
export { default as Banner } from "./Banner/Banner.svelte"
|
export { default as Banner } from "./Banner/Banner.svelte"
|
||||||
|
export { default as BannerDisplay } from "./Banner/BannerDisplay.svelte"
|
||||||
export { default as MarkdownEditor } from "./Markdown/MarkdownEditor.svelte"
|
export { default as MarkdownEditor } from "./Markdown/MarkdownEditor.svelte"
|
||||||
export { default as MarkdownViewer } from "./Markdown/MarkdownViewer.svelte"
|
export { default as MarkdownViewer } from "./Markdown/MarkdownViewer.svelte"
|
||||||
export { default as RichTextField } from "./Form/RichTextField.svelte"
|
export { default as RichTextField } from "./Form/RichTextField.svelte"
|
||||||
|
@ -84,6 +85,7 @@ export { default as clickOutside } from "./Actions/click_outside"
|
||||||
|
|
||||||
// Stores
|
// Stores
|
||||||
export { notifications, createNotificationStore } from "./Stores/notifications"
|
export { notifications, createNotificationStore } from "./Stores/notifications"
|
||||||
|
export { banner } from "./Stores/banner"
|
||||||
|
|
||||||
// Helpers
|
// Helpers
|
||||||
export * as Helpers from "./helpers"
|
export * as Helpers from "./helpers"
|
||||||
|
|
|
@ -28,7 +28,7 @@
|
||||||
chalk "^2.0.0"
|
chalk "^2.0.0"
|
||||||
js-tokens "^4.0.0"
|
js-tokens "^4.0.0"
|
||||||
|
|
||||||
"@budibase/handlebars-helpers@^0.11.7":
|
"@budibase/handlebars-helpers@^0.11.8":
|
||||||
version "0.11.8"
|
version "0.11.8"
|
||||||
resolved "https://registry.yarnpkg.com/@budibase/handlebars-helpers/-/handlebars-helpers-0.11.8.tgz#6953d29673a8c5c407e096c0a84890465c7ce841"
|
resolved "https://registry.yarnpkg.com/@budibase/handlebars-helpers/-/handlebars-helpers-0.11.8.tgz#6953d29673a8c5c407e096c0a84890465c7ce841"
|
||||||
integrity sha512-ggWJUt0GqsHFAEup5tlWlcrmYML57nKhpNGGLzVsqXVYN8eVmf3xluYmmMe7fDYhQH0leSprrdEXmsdFQF3HAQ==
|
integrity sha512-ggWJUt0GqsHFAEup5tlWlcrmYML57nKhpNGGLzVsqXVYN8eVmf3xluYmmMe7fDYhQH0leSprrdEXmsdFQF3HAQ==
|
||||||
|
@ -53,12 +53,12 @@
|
||||||
to-gfm-code-block "^0.1.1"
|
to-gfm-code-block "^0.1.1"
|
||||||
year "^0.2.1"
|
year "^0.2.1"
|
||||||
|
|
||||||
"@budibase/string-templates@^1.0.72-alpha.0":
|
"@budibase/string-templates@^1.0.84":
|
||||||
version "1.0.75"
|
version "1.0.84"
|
||||||
resolved "https://registry.yarnpkg.com/@budibase/string-templates/-/string-templates-1.0.75.tgz#5b4061f1a626160ec092f32f036541376298100c"
|
resolved "https://registry.yarnpkg.com/@budibase/string-templates/-/string-templates-1.0.84.tgz#66669e9898ed20c6be2c46a5eb19d55003eb6bcb"
|
||||||
integrity sha512-hPgr6n5cpSCGFEha5DS/P+rtRXOLc72M6y4J/scl59JvUi/ZUJkjRgJdpQPdBLu04CNKp89V59+rAqAuDjOC0g==
|
integrity sha512-6Tv/TfGkmr3uBwNdZ3eKAPKwdsRTZbuQ+02puH+EcJK2leCerINo1SpAHf1BOmjQJynKeslKpSkUiisRVerMEg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@budibase/handlebars-helpers" "^0.11.7"
|
"@budibase/handlebars-helpers" "^0.11.8"
|
||||||
dayjs "^1.10.4"
|
dayjs "^1.10.4"
|
||||||
handlebars "^4.7.6"
|
handlebars "^4.7.6"
|
||||||
handlebars-utils "^1.0.6"
|
handlebars-utils "^1.0.6"
|
||||||
|
@ -3410,9 +3410,9 @@ typo-js@*:
|
||||||
integrity sha512-bTGLjbD3WqZDR3CgEFkyi9Q/SS2oM29ipXrWfDb4M74ea69QwKAECVceYpaBu0GfdnASMg9Qfl67ttB23nePHg==
|
integrity sha512-bTGLjbD3WqZDR3CgEFkyi9Q/SS2oM29ipXrWfDb4M74ea69QwKAECVceYpaBu0GfdnASMg9Qfl67ttB23nePHg==
|
||||||
|
|
||||||
uglify-js@^3.1.4:
|
uglify-js@^3.1.4:
|
||||||
version "3.15.1"
|
version "3.15.3"
|
||||||
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.15.1.tgz#9403dc6fa5695a6172a91bc983ea39f0f7c9086d"
|
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.15.3.tgz#9aa82ca22419ba4c0137642ba0df800cb06e0471"
|
||||||
integrity sha512-FAGKF12fWdkpvNJZENacOH0e/83eG6JyVQyanIJaBXCN1J11TUQv1T1/z8S+Z0CG0ZPk1nPcreF/c7lrTd0TEQ==
|
integrity sha512-6iCVm2omGJbsu3JWac+p6kUiOpg3wFO2f8lIXjfEb8RrmLjzog1wTPMmwKB7swfzzqxj9YM+sGUM++u1qN4qJg==
|
||||||
|
|
||||||
unbox-primitive@^1.0.0:
|
unbox-primitive@^1.0.0:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
|
@ -3503,9 +3503,9 @@ vendors@^1.0.0:
|
||||||
integrity sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w==
|
integrity sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w==
|
||||||
|
|
||||||
vm2@^3.9.4:
|
vm2@^3.9.4:
|
||||||
version "3.9.8"
|
version "3.9.9"
|
||||||
resolved "https://registry.yarnpkg.com/vm2/-/vm2-3.9.8.tgz#e99c000db042735cd2f94d8db6c42163a17be04e"
|
resolved "https://registry.yarnpkg.com/vm2/-/vm2-3.9.9.tgz#c0507bc5fbb99388fad837d228badaaeb499ddc5"
|
||||||
integrity sha512-/1PYg/BwdKzMPo8maOZ0heT7DLI0DAFTm7YQaz/Lim9oIaFZsJs3EdtalvXuBfZwczNwsYhju75NW4d6E+4q+w==
|
integrity sha512-xwTm7NLh/uOjARRBs8/95H0e8fT3Ukw5D/JJWhxMbhKzNh1Nu981jQKvkep9iKYNxzlVrdzD0mlBGkDKZWprlw==
|
||||||
dependencies:
|
dependencies:
|
||||||
acorn "^8.7.0"
|
acorn "^8.7.0"
|
||||||
acorn-walk "^8.2.0"
|
acorn-walk "^8.2.0"
|
||||||
|
|
|
@ -36,10 +36,12 @@ filterTests(["smoke", "all"], () => {
|
||||||
// createRestQuery confirms query creation
|
// createRestQuery confirms query creation
|
||||||
cy.createRestQuery("GET", restUrl, "/breweries")
|
cy.createRestQuery("GET", restUrl, "/breweries")
|
||||||
// Confirm status code response within REST datasource
|
// Confirm status code response within REST datasource
|
||||||
|
cy.wait(1000)
|
||||||
|
cy.get(".stats").within(() => {
|
||||||
cy.get(".spectrum-FieldLabel")
|
cy.get(".spectrum-FieldLabel")
|
||||||
.contains("Status")
|
.eq(0)
|
||||||
.children()
|
|
||||||
.should("contain", 200)
|
.should("contain", 200)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/builder",
|
"name": "@budibase/builder",
|
||||||
"version": "1.0.80-alpha.3",
|
"version": "1.0.91-alpha.0",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -65,10 +65,10 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "^1.0.80-alpha.3",
|
"@budibase/bbui": "^1.0.91-alpha.0",
|
||||||
"@budibase/client": "^1.0.80-alpha.3",
|
"@budibase/client": "^1.0.91-alpha.0",
|
||||||
"@budibase/frontend-core": "^1.0.80-alpha.3",
|
"@budibase/frontend-core": "^1.0.91-alpha.0",
|
||||||
"@budibase/string-templates": "^1.0.80-alpha.3",
|
"@budibase/string-templates": "^1.0.91-alpha.0",
|
||||||
"@sentry/browser": "5.19.1",
|
"@sentry/browser": "5.19.1",
|
||||||
"@spectrum-css/page": "^3.0.1",
|
"@spectrum-css/page": "^3.0.1",
|
||||||
"@spectrum-css/vars": "^3.0.1",
|
"@spectrum-css/vars": "^3.0.1",
|
||||||
|
|
|
@ -1,13 +1,16 @@
|
||||||
<script>
|
<script>
|
||||||
import { Router } from "@roxi/routify"
|
import { Router } from "@roxi/routify"
|
||||||
import { routes } from "../.routify/routes"
|
import { routes } from "../.routify/routes"
|
||||||
import { NotificationDisplay } from "@budibase/bbui"
|
import { NotificationDisplay, BannerDisplay } from "@budibase/bbui"
|
||||||
import { parse, stringify } from "qs"
|
import { parse, stringify } from "qs"
|
||||||
import HelpIcon from "components/common/HelpIcon.svelte"
|
import HelpIcon from "components/common/HelpIcon.svelte"
|
||||||
|
|
||||||
const queryHandler = { parse, stringify }
|
const queryHandler = { parse, stringify }
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<div class="banner-container" />
|
||||||
|
<BannerDisplay />
|
||||||
|
|
||||||
<NotificationDisplay />
|
<NotificationDisplay />
|
||||||
<Router {routes} config={{ queryHandler }} />
|
<Router {routes} config={{ queryHandler }} />
|
||||||
<div class="modal-container" />
|
<div class="modal-container" />
|
||||||
|
|
|
@ -331,7 +331,9 @@ const getSelectedRowsBindings = asset => {
|
||||||
bindings = bindings.concat(
|
bindings = bindings.concat(
|
||||||
tables.map(table => ({
|
tables.map(table => ({
|
||||||
type: "context",
|
type: "context",
|
||||||
runtimeBinding: `${safeState}.${makePropSafe(table._id)}`,
|
runtimeBinding: `${safeState}.${makePropSafe(table._id)}.${makePropSafe(
|
||||||
|
"selectedRows"
|
||||||
|
)}`,
|
||||||
readableBinding: `${table._instanceName}.Selected rows`,
|
readableBinding: `${table._instanceName}.Selected rows`,
|
||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
|
@ -343,7 +345,9 @@ const getSelectedRowsBindings = asset => {
|
||||||
bindings = bindings.concat(
|
bindings = bindings.concat(
|
||||||
tableBlocks.map(block => ({
|
tableBlocks.map(block => ({
|
||||||
type: "context",
|
type: "context",
|
||||||
runtimeBinding: `${safeState}.${makePropSafe(block._id + "-table")}`,
|
runtimeBinding: `${safeState}.${makePropSafe(
|
||||||
|
block._id + "-table"
|
||||||
|
)}.${makePropSafe("selectedRows")}`,
|
||||||
readableBinding: `${block._instanceName}.Selected rows`,
|
readableBinding: `${block._instanceName}.Selected rows`,
|
||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
|
|
|
@ -20,7 +20,9 @@
|
||||||
$goto(`./datasource/${resp._id}`)
|
$goto(`./datasource/${resp._id}`)
|
||||||
notifications.success(`Datasource updated successfully.`)
|
notifications.success(`Datasource updated successfully.`)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
notifications.error("Error saving datasource")
|
notifications.error(err?.message ?? "Error saving datasource")
|
||||||
|
// prevent the modal from closing
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -134,8 +134,9 @@
|
||||||
|
|
||||||
// Remove all iframe event listeners on component destroy
|
// Remove all iframe event listeners on component destroy
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
if (iframe.contentWindow) {
|
|
||||||
window.removeEventListener("message", receiveMessage)
|
window.removeEventListener("message", receiveMessage)
|
||||||
|
|
||||||
|
if (iframe.contentWindow) {
|
||||||
if (!$store.clientFeatures.messagePassing) {
|
if (!$store.clientFeatures.messagePassing) {
|
||||||
// Legacy - remove in later versions of BB
|
// Legacy - remove in later versions of BB
|
||||||
iframe.contentWindow.removeEventListener(
|
iframe.contentWindow.removeEventListener(
|
||||||
|
|
|
@ -0,0 +1,86 @@
|
||||||
|
<script>
|
||||||
|
import { Label, Select, Body } from "@budibase/bbui"
|
||||||
|
import { findAllMatchingComponents } from "builderStore/componentUtils"
|
||||||
|
import { currentAsset } from "builderStore"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
|
||||||
|
export let parameters
|
||||||
|
|
||||||
|
$: tables = findAllMatchingComponents($currentAsset?.props, component =>
|
||||||
|
component._component.endsWith("table")
|
||||||
|
).map(table => ({
|
||||||
|
label: table._instanceName,
|
||||||
|
value: table._id,
|
||||||
|
}))
|
||||||
|
|
||||||
|
$: tableBlocks = findAllMatchingComponents($currentAsset?.props, component =>
|
||||||
|
component._component.endsWith("tableblock")
|
||||||
|
).map(block => ({
|
||||||
|
label: block._instanceName,
|
||||||
|
value: `${block._id}-table`,
|
||||||
|
}))
|
||||||
|
|
||||||
|
$: componentOptions = tables.concat(tableBlocks)
|
||||||
|
|
||||||
|
const FORMATS = [
|
||||||
|
{
|
||||||
|
label: "CSV",
|
||||||
|
value: "csv",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "JSON",
|
||||||
|
value: "json",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (!parameters.type) {
|
||||||
|
parameters.type = "csv"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="root">
|
||||||
|
<Body size="S">
|
||||||
|
Choose the table component that you would like to export your row selection
|
||||||
|
from.
|
||||||
|
<br />
|
||||||
|
Please ensure you have enabled row selection in the table settings.
|
||||||
|
</Body>
|
||||||
|
|
||||||
|
<div class="params">
|
||||||
|
<Label small>Table</Label>
|
||||||
|
<Select
|
||||||
|
bind:value={parameters.tableComponentId}
|
||||||
|
options={componentOptions}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Label small>Export as</Label>
|
||||||
|
<Select bind:value={parameters.type} options={FORMATS} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.root {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 500px;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(p) {
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.params {
|
||||||
|
display: grid;
|
||||||
|
column-gap: var(--spacing-xs);
|
||||||
|
row-gap: var(--spacing-s);
|
||||||
|
grid-template-columns: 70px 1fr;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -12,3 +12,4 @@ export { default as UpdateState } from "./UpdateState.svelte"
|
||||||
export { default as RefreshDataProvider } from "./RefreshDataProvider.svelte"
|
export { default as RefreshDataProvider } from "./RefreshDataProvider.svelte"
|
||||||
export { default as DuplicateRow } from "./DuplicateRow.svelte"
|
export { default as DuplicateRow } from "./DuplicateRow.svelte"
|
||||||
export { default as S3Upload } from "./S3Upload.svelte"
|
export { default as S3Upload } from "./S3Upload.svelte"
|
||||||
|
export { default as ExportData } from "./ExportData.svelte"
|
||||||
|
|
|
@ -80,6 +80,10 @@
|
||||||
"value": "publicUrl"
|
"value": "publicUrl"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Export Data",
|
||||||
|
"component": "ExportData"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
|
@ -11,7 +11,7 @@
|
||||||
import { users } from "stores/portal"
|
import { users } from "stores/portal"
|
||||||
|
|
||||||
const [email, error, touched] = createValidationStore("", emailValidator)
|
const [email, error, touched] = createValidationStore("", emailValidator)
|
||||||
const password = Math.random().toString(36).substr(2, 20)
|
const password = Math.random().toString(36).slice(2, 20)
|
||||||
let builder = false,
|
let builder = false,
|
||||||
admin = false
|
admin = false
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
|
|
||||||
export let user
|
export let user
|
||||||
|
|
||||||
const password = Math.random().toString(36).substr(2, 20)
|
const password = Math.random().toString(36).slice(2, 20)
|
||||||
|
|
||||||
async function resetPassword() {
|
async function resetPassword() {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -14,6 +14,7 @@ export function createTablesStore() {
|
||||||
...state,
|
...state,
|
||||||
list: tables,
|
list: tables,
|
||||||
}))
|
}))
|
||||||
|
return tables
|
||||||
}
|
}
|
||||||
|
|
||||||
async function select(table) {
|
async function select(table) {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { writable, get } from "svelte/store"
|
import { writable, get } from "svelte/store"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import { auth } from "stores/portal"
|
import { auth } from "stores/portal"
|
||||||
|
import { banner } from "@budibase/bbui"
|
||||||
|
|
||||||
export function createAdminStore() {
|
export function createAdminStore() {
|
||||||
const DEFAULT_CONFIG = {
|
const DEFAULT_CONFIG = {
|
||||||
|
@ -30,6 +31,13 @@ export function createAdminStore() {
|
||||||
x => x?.checked
|
x => x?.checked
|
||||||
).length
|
).length
|
||||||
await getEnvironment()
|
await getEnvironment()
|
||||||
|
|
||||||
|
// enable system status checks in the cloud
|
||||||
|
if (get(admin).cloud) {
|
||||||
|
await getSystemStatus()
|
||||||
|
checkStatus()
|
||||||
|
}
|
||||||
|
|
||||||
admin.update(store => {
|
admin.update(store => {
|
||||||
store.loaded = true
|
store.loaded = true
|
||||||
store.checklist = checklist
|
store.checklist = checklist
|
||||||
|
@ -58,6 +66,21 @@ export function createAdminStore() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const checkStatus = async () => {
|
||||||
|
const health = get(admin)?.status?.health
|
||||||
|
if (!health?.passing) {
|
||||||
|
await banner.showStatus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getSystemStatus() {
|
||||||
|
const status = await API.getSystemStatus()
|
||||||
|
admin.update(store => {
|
||||||
|
store.status = status
|
||||||
|
return store
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function unload() {
|
function unload() {
|
||||||
admin.update(store => {
|
admin.update(store => {
|
||||||
store.loaded = false
|
store.loaded = false
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/cli",
|
"name": "@budibase/cli",
|
||||||
"version": "1.0.80-alpha.3",
|
"version": "1.0.91-alpha.0",
|
||||||
"description": "Budibase CLI, for developers, self hosting and migrations.",
|
"description": "Budibase CLI, for developers, self hosting and migrations.",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/client",
|
"name": "@budibase/client",
|
||||||
"version": "1.0.80-alpha.3",
|
"version": "1.0.91-alpha.0",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"module": "dist/budibase-client.js",
|
"module": "dist/budibase-client.js",
|
||||||
"main": "dist/budibase-client.js",
|
"main": "dist/budibase-client.js",
|
||||||
|
@ -19,9 +19,9 @@
|
||||||
"dev:builder": "rollup -cw"
|
"dev:builder": "rollup -cw"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "^1.0.80-alpha.3",
|
"@budibase/bbui": "^1.0.91-alpha.0",
|
||||||
"@budibase/frontend-core": "^1.0.80-alpha.3",
|
"@budibase/frontend-core": "^1.0.91-alpha.0",
|
||||||
"@budibase/string-templates": "^1.0.80-alpha.3",
|
"@budibase/string-templates": "^1.0.91-alpha.0",
|
||||||
"@spectrum-css/button": "^3.0.3",
|
"@spectrum-css/button": "^3.0.3",
|
||||||
"@spectrum-css/card": "^3.0.3",
|
"@spectrum-css/card": "^3.0.3",
|
||||||
"@spectrum-css/divider": "^1.0.3",
|
"@spectrum-css/divider": "^1.0.3",
|
||||||
|
@ -32,6 +32,7 @@
|
||||||
"@spectrum-css/vars": "^3.0.1",
|
"@spectrum-css/vars": "^3.0.1",
|
||||||
"apexcharts": "^3.22.1",
|
"apexcharts": "^3.22.1",
|
||||||
"dayjs": "^1.10.5",
|
"dayjs": "^1.10.5",
|
||||||
|
"downloadjs": "1.4.7",
|
||||||
"regexparam": "^1.3.0",
|
"regexparam": "^1.3.0",
|
||||||
"rollup-plugin-polyfill-node": "^0.8.0",
|
"rollup-plugin-polyfill-node": "^0.8.0",
|
||||||
"shortid": "^2.2.15",
|
"shortid": "^2.2.15",
|
||||||
|
|
|
@ -81,7 +81,10 @@
|
||||||
loading = false
|
loading = false
|
||||||
return res
|
return res
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notificationStore.actions.error(`Error uploading file: ${error}`)
|
notificationStore.actions.error(
|
||||||
|
`Error uploading file: ${error?.message || error}`
|
||||||
|
)
|
||||||
|
loading = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -42,6 +42,7 @@
|
||||||
$: {
|
$: {
|
||||||
rowSelectionStore.actions.updateSelection(
|
rowSelectionStore.actions.updateSelection(
|
||||||
$component.id,
|
$component.id,
|
||||||
|
selectedRows.length ? selectedRows[0].tableId : "",
|
||||||
selectedRows.map(row => row._id)
|
selectedRows.map(row => row._id)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,20 +1,29 @@
|
||||||
import { writable } from "svelte/store"
|
import { get, writable } from "svelte/store"
|
||||||
|
|
||||||
const createRowSelectionStore = () => {
|
const createRowSelectionStore = () => {
|
||||||
const store = writable({})
|
const store = writable({})
|
||||||
|
|
||||||
function updateSelection(componentId, selectedRows) {
|
function updateSelection(componentId, tableId, selectedRows) {
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
state[componentId] = [...selectedRows]
|
state[componentId] = { tableId: tableId, selectedRows: selectedRows }
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getSelection(tableComponentId) {
|
||||||
|
const selection = get(store)
|
||||||
|
const componentId = Object.keys(selection).find(
|
||||||
|
componentId => componentId === tableComponentId
|
||||||
|
)
|
||||||
|
return selection[componentId] || {}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
subscribe: store.subscribe,
|
subscribe: store.subscribe,
|
||||||
set: store.set,
|
set: store.set,
|
||||||
actions: {
|
actions: {
|
||||||
updateSelection,
|
updateSelection,
|
||||||
|
getSelection,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
|
import download from "downloadjs"
|
||||||
import {
|
import {
|
||||||
routeStore,
|
routeStore,
|
||||||
builderStore,
|
builderStore,
|
||||||
|
@ -8,6 +9,7 @@ import {
|
||||||
notificationStore,
|
notificationStore,
|
||||||
dataSourceStore,
|
dataSourceStore,
|
||||||
uploadStore,
|
uploadStore,
|
||||||
|
rowSelectionStore,
|
||||||
} from "stores"
|
} from "stores"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import { ActionTypes } from "constants"
|
import { ActionTypes } from "constants"
|
||||||
|
@ -239,6 +241,26 @@ const s3UploadHandler = async action => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const exportDataHandler = async action => {
|
||||||
|
let selection = rowSelectionStore.actions.getSelection(
|
||||||
|
action.parameters.tableComponentId
|
||||||
|
)
|
||||||
|
if (selection.selectedRows && selection.selectedRows.length > 0) {
|
||||||
|
try {
|
||||||
|
const data = await API.exportRows({
|
||||||
|
tableId: selection.tableId,
|
||||||
|
rows: selection.selectedRows,
|
||||||
|
format: action.parameters.type,
|
||||||
|
})
|
||||||
|
download(data, `${selection.tableId}.${action.parameters.type}`)
|
||||||
|
} catch (error) {
|
||||||
|
notificationStore.actions.error("There was an error exporting the data")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
notificationStore.actions.error("Please select at least one row")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handlerMap = {
|
const handlerMap = {
|
||||||
["Save Row"]: saveRowHandler,
|
["Save Row"]: saveRowHandler,
|
||||||
["Duplicate Row"]: duplicateRowHandler,
|
["Duplicate Row"]: duplicateRowHandler,
|
||||||
|
@ -254,6 +276,7 @@ const handlerMap = {
|
||||||
["Change Form Step"]: changeFormStepHandler,
|
["Change Form Step"]: changeFormStepHandler,
|
||||||
["Update State"]: updateStateHandler,
|
["Update State"]: updateStateHandler,
|
||||||
["Upload File to S3"]: s3UploadHandler,
|
["Upload File to S3"]: s3UploadHandler,
|
||||||
|
["Export Data"]: exportDataHandler,
|
||||||
}
|
}
|
||||||
|
|
||||||
const confirmTextMap = {
|
const confirmTextMap = {
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/frontend-core",
|
"name": "@budibase/frontend-core",
|
||||||
"version": "1.0.80-alpha.3",
|
"version": "1.0.91-alpha.0",
|
||||||
"description": "Budibase frontend core libraries used in builder and client",
|
"description": "Budibase frontend core libraries used in builder and client",
|
||||||
"author": "Budibase",
|
"author": "Budibase",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"svelte": "src/index.js",
|
"svelte": "src/index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "^1.0.80-alpha.3",
|
"@budibase/bbui": "^1.0.91-alpha.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"svelte": "^3.46.2"
|
"svelte": "^3.46.2"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,20 @@
|
||||||
export const buildAttachmentEndpoints = API => ({
|
export const buildAttachmentEndpoints = API => {
|
||||||
|
/**
|
||||||
|
* Generates a signed URL to upload a file to an external datasource.
|
||||||
|
* @param datasourceId the ID of the datasource to upload to
|
||||||
|
* @param bucket the name of the bucket to upload to
|
||||||
|
* @param key the name of the file to upload to
|
||||||
|
*/
|
||||||
|
const getSignedDatasourceURL = async ({ datasourceId, bucket, key }) => {
|
||||||
|
return await API.post({
|
||||||
|
url: `/api/attachments/${datasourceId}/url`,
|
||||||
|
body: { bucket, key },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
getSignedDatasourceURL,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Uploads an attachment to the server.
|
* Uploads an attachment to the server.
|
||||||
* @param data the attachment to upload
|
* @param data the attachment to upload
|
||||||
|
@ -24,19 +40,6 @@ export const buildAttachmentEndpoints = API => ({
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates a signed URL to upload a file to an external datasource.
|
|
||||||
* @param datasourceId the ID of the datasource to upload to
|
|
||||||
* @param bucket the name of the bucket to upload to
|
|
||||||
* @param key the name of the file to upload to
|
|
||||||
*/
|
|
||||||
getSignedDatasourceURL: async ({ datasourceId, bucket, key }) => {
|
|
||||||
return await API.post({
|
|
||||||
url: `/api/attachments/${datasourceId}/url`,
|
|
||||||
body: { bucket, key },
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Uploads a file to an external datasource.
|
* Uploads a file to an external datasource.
|
||||||
* @param datasourceId the ID of the datasource to upload to
|
* @param datasourceId the ID of the datasource to upload to
|
||||||
|
@ -45,7 +48,8 @@ export const buildAttachmentEndpoints = API => ({
|
||||||
* @param data the file to upload
|
* @param data the file to upload
|
||||||
*/
|
*/
|
||||||
externalUpload: async ({ datasourceId, bucket, key, data }) => {
|
externalUpload: async ({ datasourceId, bucket, key, data }) => {
|
||||||
const { signedUrl, publicUrl } = await API.getSignedDatasourceURL({
|
console.log(API)
|
||||||
|
const { signedUrl, publicUrl } = await getSignedDatasourceURL({
|
||||||
datasourceId,
|
datasourceId,
|
||||||
bucket,
|
bucket,
|
||||||
key,
|
key,
|
||||||
|
@ -58,4 +62,5 @@ export const buildAttachmentEndpoints = API => ({
|
||||||
})
|
})
|
||||||
return { publicUrl }
|
return { publicUrl }
|
||||||
},
|
},
|
||||||
})
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -17,6 +17,15 @@ export const buildOtherEndpoints = API => ({
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the current system status.
|
||||||
|
*/
|
||||||
|
getSystemStatus: async () => {
|
||||||
|
return await API.get({
|
||||||
|
url: "/api/system/status",
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the list of available integrations.
|
* Gets the list of available integrations.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -60,4 +60,21 @@ export const buildRowEndpoints = API => ({
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exports rows.
|
||||||
|
* @param tableId the table ID to export the rows from
|
||||||
|
* @param rows the array of rows to export
|
||||||
|
*/
|
||||||
|
exportRows: async ({ tableId, rows, format }) => {
|
||||||
|
return await API.post({
|
||||||
|
url: `/api/${tableId}/rows/exportRows?format=${format}`,
|
||||||
|
body: {
|
||||||
|
rows,
|
||||||
|
},
|
||||||
|
parseResponse: async response => {
|
||||||
|
return await response.text()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/server",
|
"name": "@budibase/server",
|
||||||
"email": "hi@budibase.com",
|
"email": "hi@budibase.com",
|
||||||
"version": "1.0.80-alpha.3",
|
"version": "1.0.91-alpha.0",
|
||||||
"description": "Budibase Web Server",
|
"description": "Budibase Web Server",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -71,9 +71,9 @@
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apidevtools/swagger-parser": "^10.0.3",
|
"@apidevtools/swagger-parser": "^10.0.3",
|
||||||
"@budibase/backend-core": "^1.0.80-alpha.3",
|
"@budibase/backend-core": "^1.0.91-alpha.0",
|
||||||
"@budibase/client": "^1.0.80-alpha.3",
|
"@budibase/client": "^1.0.91-alpha.0",
|
||||||
"@budibase/string-templates": "^1.0.80-alpha.3",
|
"@budibase/string-templates": "^1.0.91-alpha.0",
|
||||||
"@bull-board/api": "^3.7.0",
|
"@bull-board/api": "^3.7.0",
|
||||||
"@bull-board/koa": "^3.7.0",
|
"@bull-board/koa": "^3.7.0",
|
||||||
"@elastic/elasticsearch": "7.10.0",
|
"@elastic/elasticsearch": "7.10.0",
|
||||||
|
|
|
@ -51,7 +51,7 @@ function extractPaths(apidocJson) {
|
||||||
// Surrounds URL parameters with curly brackets -> :email with {email}
|
// Surrounds URL parameters with curly brackets -> :email with {email}
|
||||||
let pathKeys = []
|
let pathKeys = []
|
||||||
for (let j = 1; j < matches.length; j++) {
|
for (let j = 1; j < matches.length; j++) {
|
||||||
let key = matches[j].substr(1)
|
let key = matches[j].slice(1)
|
||||||
url = url.replace(matches[j], "{" + key + "}")
|
url = url.replace(matches[j], "{" + key + "}")
|
||||||
pathKeys.push(key)
|
pathKeys.push(key)
|
||||||
}
|
}
|
||||||
|
|
|
@ -437,8 +437,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"name",
|
"name"
|
||||||
"url"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"applicationOutput": {
|
"applicationOutput": {
|
||||||
|
@ -503,6 +502,71 @@
|
||||||
"data"
|
"data"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"applicationSearch": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"description": "The name of the app.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"description": "The URL by which the app is accessed, this must be URL encoded.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"_id": {
|
||||||
|
"description": "The ID of the app.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"description": "The status of the app, stating it if is the development or published version.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"development",
|
||||||
|
"published"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"description": "States when the app was created, will be constant. Stored in ISO format.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"updatedAt": {
|
||||||
|
"description": "States the last time the app was updated - stored in ISO format.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"version": {
|
||||||
|
"description": "States the version of the Budibase client this app is currently based on.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"tenantId": {
|
||||||
|
"description": "In a multi-tenant environment this will state the tenant this app is within.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"lockedBy": {
|
||||||
|
"description": "The user this app is currently being built by.",
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"_id",
|
||||||
|
"name",
|
||||||
|
"url",
|
||||||
|
"status",
|
||||||
|
"createdAt",
|
||||||
|
"updatedAt",
|
||||||
|
"version"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"data"
|
||||||
|
]
|
||||||
|
},
|
||||||
"row": {
|
"row": {
|
||||||
"description": "The row to be created/updated, based on the table schema.",
|
"description": "The row to be created/updated, based on the table schema.",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
@ -980,6 +1044,221 @@
|
||||||
"data"
|
"data"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"tableSearch": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"description": "The table to be created/updated.",
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"name",
|
||||||
|
"schema",
|
||||||
|
"_id"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"description": "The name of the table.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"primaryDisplay": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The name of the column which should be used in relationship tags when relating to this table."
|
||||||
|
},
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"link"
|
||||||
|
],
|
||||||
|
"description": "A relationship column."
|
||||||
|
},
|
||||||
|
"constraints": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "A constraint can be applied to the column which will be validated against when a row is saved.",
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"string",
|
||||||
|
"number",
|
||||||
|
"object",
|
||||||
|
"boolean"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"presence": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Defines whether the column is required or not."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The name of the column."
|
||||||
|
},
|
||||||
|
"autocolumn": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Defines whether the column is automatically generated."
|
||||||
|
},
|
||||||
|
"fieldName": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The name of the column which a relationship column is related to in another table."
|
||||||
|
},
|
||||||
|
"tableId": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The ID of the table which a relationship column is related to."
|
||||||
|
},
|
||||||
|
"relationshipType": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"one-to-many",
|
||||||
|
"many-to-one",
|
||||||
|
"many-to-many"
|
||||||
|
],
|
||||||
|
"description": "Defines the type of relationship that this column will be used for."
|
||||||
|
},
|
||||||
|
"through": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "When using a SQL table that contains many to many relationships this defines the table the relationships are linked through."
|
||||||
|
},
|
||||||
|
"foreignKey": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "When using a SQL table that contains a one to many relationship this defines the foreign key."
|
||||||
|
},
|
||||||
|
"throughFrom": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "When using a SQL table that utilises a through table, this defines the primary key in the through table for this table."
|
||||||
|
},
|
||||||
|
"throughTo": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "When using a SQL table that utilises a through table, this defines the primary key in the through table for the related table."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"formula"
|
||||||
|
],
|
||||||
|
"description": "A formula column."
|
||||||
|
},
|
||||||
|
"constraints": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "A constraint can be applied to the column which will be validated against when a row is saved.",
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"string",
|
||||||
|
"number",
|
||||||
|
"object",
|
||||||
|
"boolean"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"presence": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Defines whether the column is required or not."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The name of the column."
|
||||||
|
},
|
||||||
|
"autocolumn": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Defines whether the column is automatically generated."
|
||||||
|
},
|
||||||
|
"formula": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Defines a Handlebars or JavaScript formula to use, note that Javascript formulas are expected to be provided in the base64 format."
|
||||||
|
},
|
||||||
|
"formulaType": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"static",
|
||||||
|
"dynamic"
|
||||||
|
],
|
||||||
|
"description": "Defines whether this is a static or dynamic formula."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"string",
|
||||||
|
"longform",
|
||||||
|
"options",
|
||||||
|
"number",
|
||||||
|
"boolean",
|
||||||
|
"array",
|
||||||
|
"datetime",
|
||||||
|
"attachment",
|
||||||
|
"link",
|
||||||
|
"formula",
|
||||||
|
"auto",
|
||||||
|
"json",
|
||||||
|
"internal"
|
||||||
|
],
|
||||||
|
"description": "Defines the type of the column, most explain themselves, a link column is a relationship."
|
||||||
|
},
|
||||||
|
"constraints": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "A constraint can be applied to the column which will be validated against when a row is saved.",
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"string",
|
||||||
|
"number",
|
||||||
|
"object",
|
||||||
|
"boolean"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"presence": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Defines whether the column is required or not."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The name of the column."
|
||||||
|
},
|
||||||
|
"autocolumn": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Defines whether the column is automatically generated."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"_id": {
|
||||||
|
"description": "The ID of the table.",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"data"
|
||||||
|
]
|
||||||
|
},
|
||||||
"executeQuery": {
|
"executeQuery": {
|
||||||
"description": "The query body must contain the required parameters for the query, this depends on query type, setup and bindings.",
|
"description": "The query body must contain the required parameters for the query, this depends on query type, setup and bindings.",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
@ -1074,6 +1353,71 @@
|
||||||
"_id"
|
"_id"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"querySearch": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"_id": {
|
||||||
|
"description": "The ID of the query.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"datasourceId": {
|
||||||
|
"description": "The ID of the data source the query belongs to.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"parameters": {
|
||||||
|
"description": "The bindings which are required to perform this query.",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fields": {
|
||||||
|
"description": "The fields that are used to perform this query, e.g. the sql statement",
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"queryVerb": {
|
||||||
|
"description": "The verb that describes this query.",
|
||||||
|
"enum": [
|
||||||
|
"create",
|
||||||
|
"read",
|
||||||
|
"update",
|
||||||
|
"delete"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"description": "The name of the query.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"schema": {
|
||||||
|
"description": "The schema of the data returned when the query is executed.",
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"transformer": {
|
||||||
|
"description": "The JavaScript transformer function, applied after the query responds with data.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"readable": {
|
||||||
|
"description": "Whether the query has readable data.",
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"name",
|
||||||
|
"schema",
|
||||||
|
"_id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"data"
|
||||||
|
]
|
||||||
|
},
|
||||||
"user": {
|
"user": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
@ -1215,6 +1559,86 @@
|
||||||
"data"
|
"data"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"userSearch": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"email": {
|
||||||
|
"description": "The email address of the user, this must be unique.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"description": "The password of the user if using password based login - this will never be returned. This can be left out of subsequent requests (updates) and will be enriched back into the user structure.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"description": "The status of the user, if they are active.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"active"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"firstName": {
|
||||||
|
"description": "The first name of the user",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"lastName": {
|
||||||
|
"description": "The last name of the user",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"forceResetPassword": {
|
||||||
|
"description": "If set to true forces the user to reset their password on first login.",
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"builder": {
|
||||||
|
"description": "Describes if the user is a builder user or not.",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"global": {
|
||||||
|
"description": "If set to true the user will be able to build any app in the system.",
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"admin": {
|
||||||
|
"description": "Describes if the user is an admin user or not.",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"global": {
|
||||||
|
"description": "If set to true the user will be able to administrate the system.",
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"roles": {
|
||||||
|
"description": "Contains the roles of the user per app (assuming they are not a builder user).",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "A map of app ID (production app ID, minus the _dev component) to a role ID, e.g. ADMIN."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"_id": {
|
||||||
|
"description": "The ID of the user.",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"email",
|
||||||
|
"roles",
|
||||||
|
"_id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"data"
|
||||||
|
]
|
||||||
|
},
|
||||||
"nameSearch": {
|
"nameSearch": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
@ -1378,11 +1802,6 @@
|
||||||
"tags": [
|
"tags": [
|
||||||
"applications"
|
"applications"
|
||||||
],
|
],
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"$ref": "#/components/parameters/appId"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"requestBody": {
|
"requestBody": {
|
||||||
"required": true,
|
"required": true,
|
||||||
"content": {
|
"content": {
|
||||||
|
@ -1399,18 +1818,7 @@
|
||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "object",
|
"$ref": "#/components/schemas/applicationSearch"
|
||||||
"required": [
|
|
||||||
"data"
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"data": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/components/schemas/application"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"examples": {
|
"examples": {
|
||||||
"applications": {
|
"applications": {
|
||||||
|
@ -1498,18 +1906,7 @@
|
||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "object",
|
"$ref": "#/components/schemas/querySearch"
|
||||||
"required": [
|
|
||||||
"data"
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"data": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/components/schemas/query"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"examples": {
|
"examples": {
|
||||||
"queries": {
|
"queries": {
|
||||||
|
@ -2025,18 +2422,7 @@
|
||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "object",
|
"$ref": "#/components/schemas/tableSearch"
|
||||||
"required": [
|
|
||||||
"data"
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"data": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/components/schemas/table"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"examples": {
|
"examples": {
|
||||||
"tables": {
|
"tables": {
|
||||||
|
@ -2203,18 +2589,7 @@
|
||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "object",
|
"$ref": "#/components/schemas/userSearch"
|
||||||
"required": [
|
|
||||||
"data"
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"data": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/components/schemas/user"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"examples": {
|
"examples": {
|
||||||
"users": {
|
"users": {
|
||||||
|
|
|
@ -309,7 +309,6 @@ components:
|
||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
- name
|
- name
|
||||||
- url
|
|
||||||
applicationOutput:
|
applicationOutput:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
@ -360,6 +359,58 @@ components:
|
||||||
- version
|
- version
|
||||||
required:
|
required:
|
||||||
- data
|
- data
|
||||||
|
applicationSearch:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
data:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
description: The name of the app.
|
||||||
|
type: string
|
||||||
|
url:
|
||||||
|
description: The URL by which the app is accessed, this must be URL encoded.
|
||||||
|
type: string
|
||||||
|
_id:
|
||||||
|
description: The ID of the app.
|
||||||
|
type: string
|
||||||
|
status:
|
||||||
|
description: The status of the app, stating it if is the development or
|
||||||
|
published version.
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- development
|
||||||
|
- published
|
||||||
|
createdAt:
|
||||||
|
description: States when the app was created, will be constant. Stored in ISO
|
||||||
|
format.
|
||||||
|
type: string
|
||||||
|
updatedAt:
|
||||||
|
description: States the last time the app was updated - stored in ISO format.
|
||||||
|
type: string
|
||||||
|
version:
|
||||||
|
description: States the version of the Budibase client this app is currently
|
||||||
|
based on.
|
||||||
|
type: string
|
||||||
|
tenantId:
|
||||||
|
description: In a multi-tenant environment this will state the tenant this app
|
||||||
|
is within.
|
||||||
|
type: string
|
||||||
|
lockedBy:
|
||||||
|
description: The user this app is currently being built by.
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- _id
|
||||||
|
- name
|
||||||
|
- url
|
||||||
|
- status
|
||||||
|
- createdAt
|
||||||
|
- updatedAt
|
||||||
|
- version
|
||||||
|
required:
|
||||||
|
- data
|
||||||
row:
|
row:
|
||||||
description: The row to be created/updated, based on the table schema.
|
description: The row to be created/updated, based on the table schema.
|
||||||
type: object
|
type: object
|
||||||
|
@ -730,6 +781,175 @@ components:
|
||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
- data
|
- data
|
||||||
|
tableSearch:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
data:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
description: The table to be created/updated.
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- name
|
||||||
|
- schema
|
||||||
|
- _id
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
description: The name of the table.
|
||||||
|
type: string
|
||||||
|
primaryDisplay:
|
||||||
|
type: string
|
||||||
|
description: The name of the column which should be used in relationship tags
|
||||||
|
when relating to this table.
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
additionalProperties:
|
||||||
|
oneOf:
|
||||||
|
- type: object
|
||||||
|
properties:
|
||||||
|
type:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- link
|
||||||
|
description: A relationship column.
|
||||||
|
constraints:
|
||||||
|
type: object
|
||||||
|
description: A constraint can be applied to the column which will be validated
|
||||||
|
against when a row is saved.
|
||||||
|
properties:
|
||||||
|
type:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- string
|
||||||
|
- number
|
||||||
|
- object
|
||||||
|
- boolean
|
||||||
|
presence:
|
||||||
|
type: boolean
|
||||||
|
description: Defines whether the column is required or not.
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
description: The name of the column.
|
||||||
|
autocolumn:
|
||||||
|
type: boolean
|
||||||
|
description: Defines whether the column is automatically generated.
|
||||||
|
fieldName:
|
||||||
|
type: string
|
||||||
|
description: The name of the column which a relationship column is related to in
|
||||||
|
another table.
|
||||||
|
tableId:
|
||||||
|
type: string
|
||||||
|
description: The ID of the table which a relationship column is related to.
|
||||||
|
relationshipType:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- one-to-many
|
||||||
|
- many-to-one
|
||||||
|
- many-to-many
|
||||||
|
description: Defines the type of relationship that this column will be used for.
|
||||||
|
through:
|
||||||
|
type: string
|
||||||
|
description: When using a SQL table that contains many to many relationships
|
||||||
|
this defines the table the relationships are linked
|
||||||
|
through.
|
||||||
|
foreignKey:
|
||||||
|
type: string
|
||||||
|
description: When using a SQL table that contains a one to many relationship
|
||||||
|
this defines the foreign key.
|
||||||
|
throughFrom:
|
||||||
|
type: string
|
||||||
|
description: When using a SQL table that utilises a through table, this defines
|
||||||
|
the primary key in the through table for this table.
|
||||||
|
throughTo:
|
||||||
|
type: string
|
||||||
|
description: When using a SQL table that utilises a through table, this defines
|
||||||
|
the primary key in the through table for the related
|
||||||
|
table.
|
||||||
|
- type: object
|
||||||
|
properties:
|
||||||
|
type:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- formula
|
||||||
|
description: A formula column.
|
||||||
|
constraints:
|
||||||
|
type: object
|
||||||
|
description: A constraint can be applied to the column which will be validated
|
||||||
|
against when a row is saved.
|
||||||
|
properties:
|
||||||
|
type:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- string
|
||||||
|
- number
|
||||||
|
- object
|
||||||
|
- boolean
|
||||||
|
presence:
|
||||||
|
type: boolean
|
||||||
|
description: Defines whether the column is required or not.
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
description: The name of the column.
|
||||||
|
autocolumn:
|
||||||
|
type: boolean
|
||||||
|
description: Defines whether the column is automatically generated.
|
||||||
|
formula:
|
||||||
|
type: string
|
||||||
|
description: Defines a Handlebars or JavaScript formula to use, note that
|
||||||
|
Javascript formulas are expected to be provided in
|
||||||
|
the base64 format.
|
||||||
|
formulaType:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- static
|
||||||
|
- dynamic
|
||||||
|
description: Defines whether this is a static or dynamic formula.
|
||||||
|
- type: object
|
||||||
|
properties:
|
||||||
|
type:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- string
|
||||||
|
- longform
|
||||||
|
- options
|
||||||
|
- number
|
||||||
|
- boolean
|
||||||
|
- array
|
||||||
|
- datetime
|
||||||
|
- attachment
|
||||||
|
- link
|
||||||
|
- formula
|
||||||
|
- auto
|
||||||
|
- json
|
||||||
|
- internal
|
||||||
|
description: Defines the type of the column, most explain themselves, a link
|
||||||
|
column is a relationship.
|
||||||
|
constraints:
|
||||||
|
type: object
|
||||||
|
description: A constraint can be applied to the column which will be validated
|
||||||
|
against when a row is saved.
|
||||||
|
properties:
|
||||||
|
type:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- string
|
||||||
|
- number
|
||||||
|
- object
|
||||||
|
- boolean
|
||||||
|
presence:
|
||||||
|
type: boolean
|
||||||
|
description: Defines whether the column is required or not.
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
description: The name of the column.
|
||||||
|
autocolumn:
|
||||||
|
type: boolean
|
||||||
|
description: Defines whether the column is automatically generated.
|
||||||
|
_id:
|
||||||
|
description: The ID of the table.
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- data
|
||||||
executeQuery:
|
executeQuery:
|
||||||
description: The query body must contain the required parameters for the query,
|
description: The query body must contain the required parameters for the query,
|
||||||
this depends on query type, setup and bindings.
|
this depends on query type, setup and bindings.
|
||||||
|
@ -803,6 +1023,55 @@ components:
|
||||||
- name
|
- name
|
||||||
- schema
|
- schema
|
||||||
- _id
|
- _id
|
||||||
|
querySearch:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
data:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
_id:
|
||||||
|
description: The ID of the query.
|
||||||
|
type: string
|
||||||
|
datasourceId:
|
||||||
|
description: The ID of the data source the query belongs to.
|
||||||
|
type: string
|
||||||
|
parameters:
|
||||||
|
description: The bindings which are required to perform this query.
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
fields:
|
||||||
|
description: The fields that are used to perform this query, e.g. the sql
|
||||||
|
statement
|
||||||
|
type: object
|
||||||
|
queryVerb:
|
||||||
|
description: The verb that describes this query.
|
||||||
|
enum:
|
||||||
|
- create
|
||||||
|
- read
|
||||||
|
- update
|
||||||
|
- delete
|
||||||
|
name:
|
||||||
|
description: The name of the query.
|
||||||
|
type: string
|
||||||
|
schema:
|
||||||
|
description: The schema of the data returned when the query is executed.
|
||||||
|
type: object
|
||||||
|
transformer:
|
||||||
|
description: The JavaScript transformer function, applied after the query
|
||||||
|
responds with data.
|
||||||
|
type: string
|
||||||
|
readable:
|
||||||
|
description: Whether the query has readable data.
|
||||||
|
type: boolean
|
||||||
|
required:
|
||||||
|
- name
|
||||||
|
- schema
|
||||||
|
- _id
|
||||||
|
required:
|
||||||
|
- data
|
||||||
user:
|
user:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
@ -916,6 +1185,69 @@ components:
|
||||||
- _id
|
- _id
|
||||||
required:
|
required:
|
||||||
- data
|
- data
|
||||||
|
userSearch:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
data:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
email:
|
||||||
|
description: The email address of the user, this must be unique.
|
||||||
|
type: string
|
||||||
|
password:
|
||||||
|
description: The password of the user if using password based login - this will
|
||||||
|
never be returned. This can be left out of subsequent requests
|
||||||
|
(updates) and will be enriched back into the user structure.
|
||||||
|
type: string
|
||||||
|
status:
|
||||||
|
description: The status of the user, if they are active.
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- active
|
||||||
|
firstName:
|
||||||
|
description: The first name of the user
|
||||||
|
type: string
|
||||||
|
lastName:
|
||||||
|
description: The last name of the user
|
||||||
|
type: string
|
||||||
|
forceResetPassword:
|
||||||
|
description: If set to true forces the user to reset their password on first
|
||||||
|
login.
|
||||||
|
type: boolean
|
||||||
|
builder:
|
||||||
|
description: Describes if the user is a builder user or not.
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
global:
|
||||||
|
description: If set to true the user will be able to build any app in the
|
||||||
|
system.
|
||||||
|
type: boolean
|
||||||
|
admin:
|
||||||
|
description: Describes if the user is an admin user or not.
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
global:
|
||||||
|
description: If set to true the user will be able to administrate the system.
|
||||||
|
type: boolean
|
||||||
|
roles:
|
||||||
|
description: Contains the roles of the user per app (assuming they are not a
|
||||||
|
builder user).
|
||||||
|
type: object
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
description: A map of app ID (production app ID, minus the _dev component) to a
|
||||||
|
role ID, e.g. ADMIN.
|
||||||
|
_id:
|
||||||
|
description: The ID of the user.
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- email
|
||||||
|
- roles
|
||||||
|
- _id
|
||||||
|
required:
|
||||||
|
- data
|
||||||
nameSearch:
|
nameSearch:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
@ -1013,8 +1345,6 @@ paths:
|
||||||
applications.
|
applications.
|
||||||
tags:
|
tags:
|
||||||
- applications
|
- applications
|
||||||
parameters:
|
|
||||||
- $ref: "#/components/parameters/appId"
|
|
||||||
requestBody:
|
requestBody:
|
||||||
required: true
|
required: true
|
||||||
content:
|
content:
|
||||||
|
@ -1028,14 +1358,7 @@ paths:
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
type: object
|
$ref: "#/components/schemas/applicationSearch"
|
||||||
required:
|
|
||||||
- data
|
|
||||||
properties:
|
|
||||||
data:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
$ref: "#/components/schemas/application"
|
|
||||||
examples:
|
examples:
|
||||||
applications:
|
applications:
|
||||||
$ref: "#/components/examples/applications"
|
$ref: "#/components/examples/applications"
|
||||||
|
@ -1087,14 +1410,7 @@ paths:
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
type: object
|
$ref: "#/components/schemas/querySearch"
|
||||||
required:
|
|
||||||
- data
|
|
||||||
properties:
|
|
||||||
data:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
$ref: "#/components/schemas/query"
|
|
||||||
examples:
|
examples:
|
||||||
queries:
|
queries:
|
||||||
$ref: "#/components/examples/queries"
|
$ref: "#/components/examples/queries"
|
||||||
|
@ -1419,14 +1735,7 @@ paths:
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
type: object
|
$ref: "#/components/schemas/tableSearch"
|
||||||
required:
|
|
||||||
- data
|
|
||||||
properties:
|
|
||||||
data:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
$ref: "#/components/schemas/table"
|
|
||||||
examples:
|
examples:
|
||||||
tables:
|
tables:
|
||||||
$ref: "#/components/examples/tables"
|
$ref: "#/components/examples/tables"
|
||||||
|
@ -1524,14 +1833,7 @@ paths:
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
type: object
|
$ref: "#/components/schemas/userSearch"
|
||||||
required:
|
|
||||||
- data
|
|
||||||
properties:
|
|
||||||
data:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
$ref: "#/components/schemas/user"
|
|
||||||
examples:
|
examples:
|
||||||
users:
|
users:
|
||||||
$ref: "#/components/examples/users"
|
$ref: "#/components/examples/users"
|
||||||
|
|
|
@ -27,7 +27,7 @@ const base = {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const applicationSchema = object(base, { required: ["name", "url"] })
|
const applicationSchema = object(base, { required: ["name"] })
|
||||||
|
|
||||||
const applicationOutputSchema = object(
|
const applicationOutputSchema = object(
|
||||||
{
|
{
|
||||||
|
@ -98,4 +98,10 @@ module.exports = new Resource()
|
||||||
applicationOutput: object({
|
applicationOutput: object({
|
||||||
data: applicationOutputSchema,
|
data: applicationOutputSchema,
|
||||||
}),
|
}),
|
||||||
|
applicationSearch: object({
|
||||||
|
data: {
|
||||||
|
type: "array",
|
||||||
|
items: applicationOutputSchema,
|
||||||
|
},
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
|
|
|
@ -186,4 +186,10 @@ module.exports = new Resource()
|
||||||
executeQuery: executeQuerySchema,
|
executeQuery: executeQuerySchema,
|
||||||
executeQueryOutput: executeQueryOutputSchema,
|
executeQueryOutput: executeQueryOutputSchema,
|
||||||
query: querySchema,
|
query: querySchema,
|
||||||
|
querySearch: object({
|
||||||
|
data: {
|
||||||
|
type: "array",
|
||||||
|
items: querySchema,
|
||||||
|
},
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
|
|
|
@ -188,4 +188,10 @@ module.exports = new Resource()
|
||||||
tableOutput: object({
|
tableOutput: object({
|
||||||
data: tableOutputSchema,
|
data: tableOutputSchema,
|
||||||
}),
|
}),
|
||||||
|
tableSearch: object({
|
||||||
|
data: {
|
||||||
|
type: "array",
|
||||||
|
items: tableOutputSchema,
|
||||||
|
},
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
|
|
|
@ -123,4 +123,10 @@ module.exports = new Resource()
|
||||||
userOutput: object({
|
userOutput: object({
|
||||||
data: userOutputSchema,
|
data: userOutputSchema,
|
||||||
}),
|
}),
|
||||||
|
userSearch: object({
|
||||||
|
data: {
|
||||||
|
type: "array",
|
||||||
|
items: userOutputSchema,
|
||||||
|
},
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { default as rowController } from "../row"
|
import { default as rowController } from "../row"
|
||||||
import { addRev } from "./utils"
|
import { addRev } from "./utils"
|
||||||
import { Row } from "../../../definitions/common"
|
import { Row } from "../../../definitions/common"
|
||||||
|
import { convertBookmark } from "../../../utilities"
|
||||||
|
|
||||||
// makes sure that the user doesn't need to pass in the type, tableId or _id params for
|
// makes sure that the user doesn't need to pass in the type, tableId or _id params for
|
||||||
// the call to be correct
|
// the call to be correct
|
||||||
|
@ -30,7 +31,7 @@ export async function search(ctx: any, next: any) {
|
||||||
sort: sort.column,
|
sort: sort.column,
|
||||||
sortType: sort.type,
|
sortType: sort.type,
|
||||||
sortOrder: sort.order,
|
sortOrder: sort.order,
|
||||||
bookmark,
|
bookmark: convertBookmark(bookmark),
|
||||||
paginate,
|
paginate,
|
||||||
limit,
|
limit,
|
||||||
query,
|
query,
|
||||||
|
|
|
@ -10,6 +10,8 @@ const {
|
||||||
} = require("../../../integrations/utils")
|
} = require("../../../integrations/utils")
|
||||||
const ExternalRequest = require("./ExternalRequest")
|
const ExternalRequest = require("./ExternalRequest")
|
||||||
const { getAppDB } = require("@budibase/backend-core/context")
|
const { getAppDB } = require("@budibase/backend-core/context")
|
||||||
|
const exporters = require("../view/exporters")
|
||||||
|
const { apiFileReturn } = require("../../../utilities/fileSystem")
|
||||||
|
|
||||||
async function handleRequest(operation, tableId, opts = {}) {
|
async function handleRequest(operation, tableId, opts = {}) {
|
||||||
// make sure the filters are cleaned up, no empty strings for equals, fuzzy or string
|
// make sure the filters are cleaned up, no empty strings for equals, fuzzy or string
|
||||||
|
@ -33,11 +35,11 @@ exports.handleRequest = handleRequest
|
||||||
exports.patch = async ctx => {
|
exports.patch = async ctx => {
|
||||||
const inputs = ctx.request.body
|
const inputs = ctx.request.body
|
||||||
const tableId = ctx.params.tableId
|
const tableId = ctx.params.tableId
|
||||||
const id = breakRowIdField(inputs._id)
|
const id = inputs._id
|
||||||
// don't save the ID to db
|
// don't save the ID to db
|
||||||
delete inputs._id
|
delete inputs._id
|
||||||
return handleRequest(DataSourceOperation.UPDATE, tableId, {
|
return handleRequest(DataSourceOperation.UPDATE, tableId, {
|
||||||
id,
|
id: breakRowIdField(id),
|
||||||
row: inputs,
|
row: inputs,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -67,7 +69,7 @@ exports.find = async ctx => {
|
||||||
const id = ctx.params.rowId
|
const id = ctx.params.rowId
|
||||||
const tableId = ctx.params.tableId
|
const tableId = ctx.params.tableId
|
||||||
const response = await handleRequest(DataSourceOperation.READ, tableId, {
|
const response = await handleRequest(DataSourceOperation.READ, tableId, {
|
||||||
id,
|
id: breakRowIdField(id),
|
||||||
})
|
})
|
||||||
return response ? response[0] : response
|
return response ? response[0] : response
|
||||||
}
|
}
|
||||||
|
@ -76,7 +78,7 @@ exports.destroy = async ctx => {
|
||||||
const tableId = ctx.params.tableId
|
const tableId = ctx.params.tableId
|
||||||
const id = ctx.request.body._id
|
const id = ctx.request.body._id
|
||||||
const { row } = await handleRequest(DataSourceOperation.DELETE, tableId, {
|
const { row } = await handleRequest(DataSourceOperation.DELETE, tableId, {
|
||||||
id,
|
id: breakRowIdField(id),
|
||||||
})
|
})
|
||||||
return { response: { ok: true }, row }
|
return { response: { ok: true }, row }
|
||||||
}
|
}
|
||||||
|
@ -152,6 +154,37 @@ exports.validate = async () => {
|
||||||
return { valid: true }
|
return { valid: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.exportRows = async ctx => {
|
||||||
|
const { datasourceId, tableName } = breakExternalTableId(ctx.params.tableId)
|
||||||
|
const db = getAppDB()
|
||||||
|
let format = ctx.query.format
|
||||||
|
const datasource = await db.get(datasourceId)
|
||||||
|
if (!datasource || !datasource.entities) {
|
||||||
|
ctx.throw(400, "Datasource has not been configured for plus API.")
|
||||||
|
}
|
||||||
|
const tables = datasource.entities
|
||||||
|
const table = tables[tableName]
|
||||||
|
ctx.request.body = {
|
||||||
|
query: {
|
||||||
|
oneOf: {
|
||||||
|
[table.primaryDisplay]: ctx.request.body.rows.map(
|
||||||
|
id => breakRowIdField(id)[0]
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = await exports.search(ctx)
|
||||||
|
|
||||||
|
let headers = Object.keys(result.rows[0])
|
||||||
|
const exporter = exporters[format]
|
||||||
|
const filename = `export.${format}`
|
||||||
|
|
||||||
|
// send down the file
|
||||||
|
ctx.attachment(filename)
|
||||||
|
return apiFileReturn(exporter(headers, result.rows))
|
||||||
|
}
|
||||||
|
|
||||||
exports.fetchEnrichedRow = async ctx => {
|
exports.fetchEnrichedRow = async ctx => {
|
||||||
const id = ctx.params.rowId
|
const id = ctx.params.rowId
|
||||||
const tableId = ctx.params.tableId
|
const tableId = ctx.params.tableId
|
||||||
|
|
|
@ -137,3 +137,12 @@ exports.fetchEnrichedRow = async function (ctx) {
|
||||||
ctx.throw(400, err)
|
ctx.throw(400, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.export = async function (ctx) {
|
||||||
|
const tableId = getTableId(ctx)
|
||||||
|
try {
|
||||||
|
ctx.body = await pickApi(tableId).exportRows(ctx)
|
||||||
|
} catch (err) {
|
||||||
|
ctx.throw(400, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -27,6 +27,8 @@ const {
|
||||||
const { cloneDeep } = require("lodash/fp")
|
const { cloneDeep } = require("lodash/fp")
|
||||||
const { getAppDB } = require("@budibase/backend-core/context")
|
const { getAppDB } = require("@budibase/backend-core/context")
|
||||||
const { finaliseRow, updateRelatedFormula } = require("./staticFormula")
|
const { finaliseRow, updateRelatedFormula } = require("./staticFormula")
|
||||||
|
const exporters = require("../view/exporters")
|
||||||
|
const { apiFileReturn } = require("../../../utilities/fileSystem")
|
||||||
|
|
||||||
const CALCULATION_TYPES = {
|
const CALCULATION_TYPES = {
|
||||||
SUM: "sum",
|
SUM: "sum",
|
||||||
|
@ -362,6 +364,29 @@ exports.validate = async ctx => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.exportRows = async ctx => {
|
||||||
|
const db = getAppDB()
|
||||||
|
const table = await db.get(ctx.params.tableId)
|
||||||
|
const rowIds = ctx.request.body.rows
|
||||||
|
let format = ctx.query.format
|
||||||
|
let response = (
|
||||||
|
await db.allDocs({
|
||||||
|
include_docs: true,
|
||||||
|
keys: rowIds,
|
||||||
|
})
|
||||||
|
).rows.map(row => row.doc)
|
||||||
|
|
||||||
|
let rows = await outputProcessing(table, response)
|
||||||
|
|
||||||
|
let headers = Object.keys(rows[0])
|
||||||
|
const exporter = exporters[format]
|
||||||
|
const filename = `export.${format}`
|
||||||
|
|
||||||
|
// send down the file
|
||||||
|
ctx.attachment(filename)
|
||||||
|
return apiFileReturn(exporter(headers, rows))
|
||||||
|
}
|
||||||
|
|
||||||
exports.fetchEnrichedRow = async ctx => {
|
exports.fetchEnrichedRow = async ctx => {
|
||||||
const db = getAppDB()
|
const db = getAppDB()
|
||||||
const tableId = ctx.params.tableId
|
const tableId = ctx.params.tableId
|
||||||
|
|
|
@ -25,7 +25,8 @@ async function makeTableRequest(
|
||||||
operation,
|
operation,
|
||||||
table,
|
table,
|
||||||
tables,
|
tables,
|
||||||
oldTable = null
|
oldTable = null,
|
||||||
|
renamed = null
|
||||||
) {
|
) {
|
||||||
const json = {
|
const json = {
|
||||||
endpoint: {
|
endpoint: {
|
||||||
|
@ -41,6 +42,9 @@ async function makeTableRequest(
|
||||||
if (oldTable) {
|
if (oldTable) {
|
||||||
json.meta.table = oldTable
|
json.meta.table = oldTable
|
||||||
}
|
}
|
||||||
|
if (renamed) {
|
||||||
|
json.meta.renamed = renamed
|
||||||
|
}
|
||||||
return makeExternalQuery(datasource, json)
|
return makeExternalQuery(datasource, json)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -160,6 +164,7 @@ function isRelationshipSetup(column) {
|
||||||
|
|
||||||
exports.save = async function (ctx) {
|
exports.save = async function (ctx) {
|
||||||
const table = ctx.request.body
|
const table = ctx.request.body
|
||||||
|
const { _rename: renamed } = table
|
||||||
// can't do this right now
|
// can't do this right now
|
||||||
delete table.dataImport
|
delete table.dataImport
|
||||||
const datasourceId = getDatasourceId(ctx.request.body)
|
const datasourceId = getDatasourceId(ctx.request.body)
|
||||||
|
@ -241,7 +246,14 @@ exports.save = async function (ctx) {
|
||||||
const operation = oldTable
|
const operation = oldTable
|
||||||
? DataSourceOperation.UPDATE_TABLE
|
? DataSourceOperation.UPDATE_TABLE
|
||||||
: DataSourceOperation.CREATE_TABLE
|
: DataSourceOperation.CREATE_TABLE
|
||||||
await makeTableRequest(datasource, operation, tableToSave, tables, oldTable)
|
await makeTableRequest(
|
||||||
|
datasource,
|
||||||
|
operation,
|
||||||
|
tableToSave,
|
||||||
|
tables,
|
||||||
|
oldTable,
|
||||||
|
renamed
|
||||||
|
)
|
||||||
// update any extra tables (like foreign keys in other tables)
|
// update any extra tables (like foreign keys in other tables)
|
||||||
for (let extraTable of extraTablesToUpdate) {
|
for (let extraTable of extraTablesToUpdate) {
|
||||||
const oldExtraTable = oldTables[extraTable.name]
|
const oldExtraTable = oldTables[extraTable.name]
|
||||||
|
@ -258,6 +270,8 @@ exports.save = async function (ctx) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// remove the rename prop
|
||||||
|
delete tableToSave._rename
|
||||||
// store it into couch now for budibase reference
|
// store it into couch now for budibase reference
|
||||||
datasource.entities[tableToSave.name] = tableToSave
|
datasource.entities[tableToSave.name] = tableToSave
|
||||||
await db.put(datasource)
|
await db.put(datasource)
|
||||||
|
|
|
@ -121,8 +121,6 @@ read.push(new Endpoint("get", "/applications/:appId", controller.read))
|
||||||
* description: Based on application properties (currently only name) search for applications.
|
* description: Based on application properties (currently only name) search for applications.
|
||||||
* tags:
|
* tags:
|
||||||
* - applications
|
* - applications
|
||||||
* parameters:
|
|
||||||
* - $ref: '#/components/parameters/appId'
|
|
||||||
* requestBody:
|
* requestBody:
|
||||||
* required: true
|
* required: true
|
||||||
* content:
|
* content:
|
||||||
|
@ -135,14 +133,7 @@ read.push(new Endpoint("get", "/applications/:appId", controller.read))
|
||||||
* content:
|
* content:
|
||||||
* application/json:
|
* application/json:
|
||||||
* schema:
|
* schema:
|
||||||
* type: object
|
* $ref: '#/components/schemas/applicationSearch'
|
||||||
* required:
|
|
||||||
* - data
|
|
||||||
* properties:
|
|
||||||
* data:
|
|
||||||
* type: array
|
|
||||||
* items:
|
|
||||||
* $ref: '#/components/schemas/application'
|
|
||||||
* examples:
|
* examples:
|
||||||
* applications:
|
* applications:
|
||||||
* $ref: '#/components/examples/applications'
|
* $ref: '#/components/examples/applications'
|
||||||
|
|
|
@ -5,6 +5,7 @@ import rowEndpoints from "./rows"
|
||||||
import userEndpoints from "./users"
|
import userEndpoints from "./users"
|
||||||
import usage from "../../../middleware/usageQuota"
|
import usage from "../../../middleware/usageQuota"
|
||||||
import authorized from "../../../middleware/authorized"
|
import authorized from "../../../middleware/authorized"
|
||||||
|
import publicApi from "../../../middleware/publicApi"
|
||||||
import { paramResource, paramSubResource } from "../../../middleware/resourceId"
|
import { paramResource, paramSubResource } from "../../../middleware/resourceId"
|
||||||
import { CtxFn } from "./utils/Endpoint"
|
import { CtxFn } from "./utils/Endpoint"
|
||||||
import mapperMiddleware from "./middleware/mapper"
|
import mapperMiddleware from "./middleware/mapper"
|
||||||
|
@ -31,16 +32,24 @@ function getApiLimitPerSecond(): number {
|
||||||
|
|
||||||
/*if (!env.isTest()) {
|
/*if (!env.isTest()) {
|
||||||
const REDIS_OPTS = getRedisOptions()
|
const REDIS_OPTS = getRedisOptions()
|
||||||
RateLimit.defaultOptions({
|
let options
|
||||||
store: new Stores.Redis({
|
if (REDIS_OPTS.redisProtocolUrl) {
|
||||||
// @ts-ignore
|
// fully qualified redis URL
|
||||||
|
options = {
|
||||||
|
url: REDIS_OPTS.redisProtocolUrl,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
options = {
|
||||||
socket: {
|
socket: {
|
||||||
host: REDIS_OPTS.host,
|
host: REDIS_OPTS.host,
|
||||||
port: REDIS_OPTS.port,
|
port: REDIS_OPTS.port,
|
||||||
},
|
},
|
||||||
password: REDIS_OPTS.opts.password,
|
password: REDIS_OPTS.opts.password,
|
||||||
database: 1,
|
database: 1,
|
||||||
}),
|
}
|
||||||
|
}
|
||||||
|
RateLimit.defaultOptions({
|
||||||
|
store: new Stores.Redis(options),
|
||||||
})
|
})
|
||||||
}*/
|
}*/
|
||||||
// rate limiting, allows for 2 requests per second
|
// rate limiting, allows for 2 requests per second
|
||||||
|
@ -93,6 +102,12 @@ function applyRoutes(
|
||||||
const paramMiddleware = subResource
|
const paramMiddleware = subResource
|
||||||
? paramSubResource(resource, subResource)
|
? paramSubResource(resource, subResource)
|
||||||
: paramResource(resource)
|
: paramResource(resource)
|
||||||
|
const publicApiMiddleware = publicApi({
|
||||||
|
requiresAppId:
|
||||||
|
permType !== PermissionTypes.APP && permType !== PermissionTypes.USER,
|
||||||
|
})
|
||||||
|
addMiddleware(endpoints.read, publicApiMiddleware)
|
||||||
|
addMiddleware(endpoints.write, publicApiMiddleware)
|
||||||
// add the parameter capture middleware
|
// add the parameter capture middleware
|
||||||
addMiddleware(endpoints.read, paramMiddleware)
|
addMiddleware(endpoints.read, paramMiddleware)
|
||||||
addMiddleware(endpoints.write, paramMiddleware)
|
addMiddleware(endpoints.write, paramMiddleware)
|
||||||
|
|
|
@ -60,14 +60,7 @@ write.push(new Endpoint("post", "/queries/:queryId", controller.execute))
|
||||||
* content:
|
* content:
|
||||||
* application/json:
|
* application/json:
|
||||||
* schema:
|
* schema:
|
||||||
* type: object
|
* $ref: '#/components/schemas/querySearch'
|
||||||
* required:
|
|
||||||
* - data
|
|
||||||
* properties:
|
|
||||||
* data:
|
|
||||||
* type: array
|
|
||||||
* items:
|
|
||||||
* $ref: '#/components/schemas/query'
|
|
||||||
* examples:
|
* examples:
|
||||||
* queries:
|
* queries:
|
||||||
* $ref: '#/components/examples/queries'
|
* $ref: '#/components/examples/queries'
|
||||||
|
|
|
@ -148,14 +148,7 @@ read.push(new Endpoint("get", "/tables/:tableId", controller.read))
|
||||||
* content:
|
* content:
|
||||||
* application/json:
|
* application/json:
|
||||||
* schema:
|
* schema:
|
||||||
* type: object
|
* $ref: '#/components/schemas/tableSearch'
|
||||||
* required:
|
|
||||||
* - data
|
|
||||||
* properties:
|
|
||||||
* data:
|
|
||||||
* type: array
|
|
||||||
* items:
|
|
||||||
* $ref: '#/components/schemas/table'
|
|
||||||
* examples:
|
* examples:
|
||||||
* tables:
|
* tables:
|
||||||
* $ref: '#/components/examples/tables'
|
* $ref: '#/components/examples/tables'
|
||||||
|
|
|
@ -123,14 +123,7 @@ read.push(new Endpoint("get", "/users/:userId", controller.read))
|
||||||
* content:
|
* content:
|
||||||
* application/json:
|
* application/json:
|
||||||
* schema:
|
* schema:
|
||||||
* type: object
|
* $ref: '#/components/schemas/userSearch'
|
||||||
* required:
|
|
||||||
* - data
|
|
||||||
* properties:
|
|
||||||
* data:
|
|
||||||
* type: array
|
|
||||||
* items:
|
|
||||||
* $ref: '#/components/schemas/user'
|
|
||||||
* examples:
|
* examples:
|
||||||
* users:
|
* users:
|
||||||
* $ref: '#/components/examples/users'
|
* $ref: '#/components/examples/users'
|
||||||
|
|
|
@ -252,4 +252,25 @@ router
|
||||||
rowController.destroy
|
rowController.destroy
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {post} /api/:tableId/rows/exportRows Export Rows
|
||||||
|
* @apiName Export rows
|
||||||
|
* @apiGroup rows
|
||||||
|
* @apiPermission table write access
|
||||||
|
* @apiDescription This API can export a number of provided rows
|
||||||
|
*
|
||||||
|
* @apiParam {string} tableId The ID of the table the row is to be deleted from.
|
||||||
|
*
|
||||||
|
* @apiParam (Body) {object[]} [rows] The row IDs which are to be exported
|
||||||
|
*
|
||||||
|
* @apiSuccess {object[]|object}
|
||||||
|
*/
|
||||||
|
.post(
|
||||||
|
"/api/:tableId/rows/exportRows",
|
||||||
|
paramResource("tableId"),
|
||||||
|
authorized(PermissionTypes.TABLE, PermissionLevels.WRITE),
|
||||||
|
usage,
|
||||||
|
rowController.export
|
||||||
|
)
|
||||||
|
|
||||||
module.exports = router
|
module.exports = router
|
||||||
|
|
|
@ -139,6 +139,11 @@ export interface PaginationJson {
|
||||||
page?: string | number
|
page?: string | number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RenameColumn {
|
||||||
|
old: string
|
||||||
|
updated: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface RelationshipsJson {
|
export interface RelationshipsJson {
|
||||||
through?: string
|
through?: string
|
||||||
from?: string
|
from?: string
|
||||||
|
@ -167,6 +172,7 @@ export interface QueryJson {
|
||||||
meta?: {
|
meta?: {
|
||||||
table?: Table
|
table?: Table
|
||||||
tables?: Record<string, Table>
|
tables?: Record<string, Table>
|
||||||
|
renamed: RenameColumn
|
||||||
}
|
}
|
||||||
extra?: {
|
extra?: {
|
||||||
idFilter?: SearchFilters
|
idFilter?: SearchFilters
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -8,5 +8,6 @@ export interface DatasourcePlus extends IntegrationBase {
|
||||||
// if the datasource supports the use of bindings directly (to protect against SQL injection)
|
// if the datasource supports the use of bindings directly (to protect against SQL injection)
|
||||||
// this returns the format of the identifier
|
// this returns the format of the identifier
|
||||||
getBindingIdentifier(): string
|
getBindingIdentifier(): string
|
||||||
|
getStringConcat(parts: string[]): string
|
||||||
buildSchema(datasourceId: string, entities: Record<string, Table>): any
|
buildSchema(datasourceId: string, entities: Record<string, Table>): any
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
import { Knex, knex } from "knex"
|
import { Knex, knex } from "knex"
|
||||||
import { Table } from "../../definitions/common"
|
import { Table } from "../../definitions/common"
|
||||||
import { Operation, QueryJson } from "../../definitions/datasource"
|
import {
|
||||||
|
Operation,
|
||||||
|
QueryJson,
|
||||||
|
RenameColumn,
|
||||||
|
} from "../../definitions/datasource"
|
||||||
import { breakExternalTableId } from "../utils"
|
import { breakExternalTableId } from "../utils"
|
||||||
import SchemaBuilder = Knex.SchemaBuilder
|
import SchemaBuilder = Knex.SchemaBuilder
|
||||||
import CreateTableBuilder = Knex.CreateTableBuilder
|
import CreateTableBuilder = Knex.CreateTableBuilder
|
||||||
|
@ -10,7 +14,8 @@ function generateSchema(
|
||||||
schema: CreateTableBuilder,
|
schema: CreateTableBuilder,
|
||||||
table: Table,
|
table: Table,
|
||||||
tables: Record<string, Table>,
|
tables: Record<string, Table>,
|
||||||
oldTable: null | Table = null
|
oldTable: null | Table = null,
|
||||||
|
renamed?: RenameColumn
|
||||||
) {
|
) {
|
||||||
let primaryKey = table && table.primary ? table.primary[0] : null
|
let primaryKey = table && table.primary ? table.primary[0] : null
|
||||||
const columns = Object.values(table.schema)
|
const columns = Object.values(table.schema)
|
||||||
|
@ -29,7 +34,11 @@ function generateSchema(
|
||||||
for (let [key, column] of Object.entries(table.schema)) {
|
for (let [key, column] of Object.entries(table.schema)) {
|
||||||
// skip things that are already correct
|
// skip things that are already correct
|
||||||
const oldColumn = oldTable ? oldTable.schema[key] : null
|
const oldColumn = oldTable ? oldTable.schema[key] : null
|
||||||
if ((oldColumn && oldColumn.type) || (primaryKey === key && !isJunction)) {
|
if (
|
||||||
|
(oldColumn && oldColumn.type) ||
|
||||||
|
(primaryKey === key && !isJunction) ||
|
||||||
|
renamed?.updated === key
|
||||||
|
) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
switch (column.type) {
|
switch (column.type) {
|
||||||
|
@ -81,6 +90,10 @@ function generateSchema(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (renamed) {
|
||||||
|
schema.renameColumn(renamed.old, renamed.updated)
|
||||||
|
}
|
||||||
|
|
||||||
// need to check if any columns have been deleted
|
// need to check if any columns have been deleted
|
||||||
if (oldTable) {
|
if (oldTable) {
|
||||||
const deletedColumns = Object.entries(oldTable.schema)
|
const deletedColumns = Object.entries(oldTable.schema)
|
||||||
|
@ -90,6 +103,9 @@ function generateSchema(
|
||||||
)
|
)
|
||||||
.map(([key]) => key)
|
.map(([key]) => key)
|
||||||
deletedColumns.forEach(key => {
|
deletedColumns.forEach(key => {
|
||||||
|
if (renamed?.old === key) {
|
||||||
|
return
|
||||||
|
}
|
||||||
if (oldTable.constrained && oldTable.constrained.indexOf(key) !== -1) {
|
if (oldTable.constrained && oldTable.constrained.indexOf(key) !== -1) {
|
||||||
schema.dropForeign(key)
|
schema.dropForeign(key)
|
||||||
}
|
}
|
||||||
|
@ -114,10 +130,11 @@ function buildUpdateTable(
|
||||||
knex: SchemaBuilder,
|
knex: SchemaBuilder,
|
||||||
table: Table,
|
table: Table,
|
||||||
tables: Record<string, Table>,
|
tables: Record<string, Table>,
|
||||||
oldTable: Table
|
oldTable: Table,
|
||||||
|
renamed: RenameColumn
|
||||||
): SchemaBuilder {
|
): SchemaBuilder {
|
||||||
return knex.alterTable(table.name, schema => {
|
return knex.alterTable(table.name, schema => {
|
||||||
generateSchema(schema, table, tables, oldTable)
|
generateSchema(schema, table, tables, oldTable, renamed)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -167,7 +184,8 @@ class SqlTableQueryBuilder {
|
||||||
client,
|
client,
|
||||||
json.table,
|
json.table,
|
||||||
json.meta.tables,
|
json.meta.tables,
|
||||||
json.meta.table
|
json.meta.table,
|
||||||
|
json.meta.renamed
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
case Operation.DELETE_TABLE:
|
case Operation.DELETE_TABLE:
|
||||||
|
|
|
@ -115,6 +115,10 @@ module GoogleSheetsModule {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getStringConcat(parts: string[]) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pull the spreadsheet ID out from a valid google sheets URL
|
* Pull the spreadsheet ID out from a valid google sheets URL
|
||||||
* @param spreadsheetId - the URL or standard spreadsheetId of the google sheet
|
* @param spreadsheetId - the URL or standard spreadsheetId of the google sheet
|
||||||
|
|
|
@ -126,7 +126,11 @@ module MSSQLModule {
|
||||||
}
|
}
|
||||||
|
|
||||||
getBindingIdentifier(): string {
|
getBindingIdentifier(): string {
|
||||||
return `(@p${this.index++})`
|
return `@p${this.index++}`
|
||||||
|
}
|
||||||
|
|
||||||
|
getStringConcat(parts: string[]): string {
|
||||||
|
return `concat(${parts.join(", ")})`
|
||||||
}
|
}
|
||||||
|
|
||||||
async connect() {
|
async connect() {
|
||||||
|
|
|
@ -99,6 +99,10 @@ module MySQLModule {
|
||||||
return "?"
|
return "?"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getStringConcat(parts: string[]): string {
|
||||||
|
return `concat(${parts.join(", ")})`
|
||||||
|
}
|
||||||
|
|
||||||
async connect() {
|
async connect() {
|
||||||
this.client = await mysql.createConnection(this.config)
|
this.client = await mysql.createConnection(this.config)
|
||||||
}
|
}
|
||||||
|
|
|
@ -179,6 +179,10 @@ module OracleModule {
|
||||||
return `:${this.index++}`
|
return `:${this.index++}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getStringConcat(parts: string[]): string {
|
||||||
|
return parts.join(" || ")
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Map the flat tabular columns and constraints data into a nested object
|
* Map the flat tabular columns and constraints data into a nested object
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -148,6 +148,10 @@ module PostgresModule {
|
||||||
return `$${this.index++}`
|
return `$${this.index++}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getStringConcat(parts: string[]): string {
|
||||||
|
return parts.join(" || ")
|
||||||
|
}
|
||||||
|
|
||||||
async internalQuery(query: SqlQuery) {
|
async internalQuery(query: SqlQuery) {
|
||||||
const client = this.client
|
const client = this.client
|
||||||
this.index = 1
|
this.index = 1
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
const { Headers } = require("@budibase/backend-core/constants")
|
||||||
|
const { getAppId } = require("@budibase/backend-core/utils")
|
||||||
|
|
||||||
|
module.exports = function ({ requiresAppId } = {}) {
|
||||||
|
return async (ctx, next) => {
|
||||||
|
const appId = getAppId(ctx)
|
||||||
|
if (requiresAppId && !appId) {
|
||||||
|
ctx.throw(
|
||||||
|
400,
|
||||||
|
`Invalid app ID provided, please check the ${Headers.APP_ID} header.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (!ctx.headers[Headers.API_KEY]) {
|
||||||
|
ctx.throw(
|
||||||
|
400,
|
||||||
|
`Invalid API key provided, please check the ${Headers.API_KEY} header.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue