- 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.
@@ -40,9 +40,11 @@
- Get started
+ Get started - we host (Budibase Cloud) ·
- Docs
+ Get started - you host (Docker, K8s, DO)
+ ·
+ Docs · Feature request ·
@@ -100,16 +102,45 @@ Budibase is made to scale. With Budibase, you can self-host on your own infrastr
- Checkout the promo video: https://youtu.be/xoljVpty_Kw
+
+
+---
+
+
+
+
+## Budibase Public API
+As with anything that we build in Budibase, our new public API is simple to use, flexible, and introduces new extensibility. To summarize, the Budibase API enables:
+
+- Budibase as a backend
+- Interoperability
+
+
+#### Docs
+You can learn more about the Budibase API at the following places:
+
+- [General documentation](https://docs.budibase.com/docs/public-api) : Learn how to get your API key, how to use spec, and how to use with Postman
+- [Interactive API documentation](https://docs.budibase.com/reference/post_applications) : Learn how to interact with the API
+
+#### Guides
+
+- [Build an app with Budibase and Next.js](https://budibase.com/blog/building-a-crud-app-with-budibase-and-next.js/)
+
+
+
+
+
+
## 🏁 Get started
-
+
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.
-### [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)
@@ -118,7 +149,7 @@ Or use Budibase Cloud if you don't need to self-host, and would like to get star
## 🎓 Learning Budibase
-The Budibase documentation [lives here](https://docs.budibase.com).
+The Budibase documentation [lives here](https://docs.budibase.com/docs).
diff --git a/charts/budibase/Chart.yaml b/charts/budibase/Chart.yaml
index daf8dad635..134d29441f 100644
--- a/charts/budibase/Chart.yaml
+++ b/charts/budibase/Chart.yaml
@@ -15,7 +15,7 @@ version: 0.2.8
appVersion: 1.0.48
dependencies:
- name: couchdb
- version: 3.3.4
+ version: 3.6.1
repository: https://apache.github.io/couchdb-helm
condition: services.couchdb.enabled
- name: ingress-nginx
diff --git a/charts/budibase/templates/app-service-deployment.yaml b/charts/budibase/templates/app-service-deployment.yaml
index d9def8c641..86e255d331 100644
--- a/charts/budibase/templates/app-service-deployment.yaml
+++ b/charts/budibase/templates/app-service-deployment.yaml
@@ -110,6 +110,10 @@ spec:
value: {{ .Values.globals.cookieDomain | quote }}
- name: HTTP_MIGRATIONS
value: {{ .Values.globals.httpMigrations | quote }}
+ - name: GOOGLE_CLIENT_ID
+ value: {{ .Values.globals.google.clientId | quote }}
+ - name: GOOGLE_CLIENT_SECRET
+ value: {{ .Values.globals.google.secret | quote }}
image: budibase/apps:{{ .Values.globals.appVersion }}
imagePullPolicy: Always
name: bbapps
diff --git a/examples/nextjs-api-sales/.eslintrc.json b/examples/nextjs-api-sales/.eslintrc.json
new file mode 100644
index 0000000000..bffb357a71
--- /dev/null
+++ b/examples/nextjs-api-sales/.eslintrc.json
@@ -0,0 +1,3 @@
+{
+ "extends": "next/core-web-vitals"
+}
diff --git a/examples/nextjs-api-sales/.gitignore b/examples/nextjs-api-sales/.gitignore
new file mode 100644
index 0000000000..7d093c39ff
--- /dev/null
+++ b/examples/nextjs-api-sales/.gitignore
@@ -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
diff --git a/examples/nextjs-api-sales/README.md b/examples/nextjs-api-sales/README.md
new file mode 100644
index 0000000000..8a466206e7
--- /dev/null
+++ b/examples/nextjs-api-sales/README.md
@@ -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).
diff --git a/examples/nextjs-api-sales/components/layout.tsx b/examples/nextjs-api-sales/components/layout.tsx
new file mode 100644
index 0000000000..48052028d7
--- /dev/null
+++ b/examples/nextjs-api-sales/components/layout.tsx
@@ -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 (
+ <>
+
+
+ {props.children}
+ >
+ )
+}
+
+export default layout
\ No newline at end of file
diff --git a/examples/nextjs-api-sales/components/notifications.ts b/examples/nextjs-api-sales/components/notifications.ts
new file mode 100644
index 0000000000..991f3f6cf2
--- /dev/null
+++ b/examples/nextjs-api-sales/components/notifications.ts
@@ -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
diff --git a/examples/nextjs-api-sales/db/docker-compose.yml b/examples/nextjs-api-sales/db/docker-compose.yml
new file mode 100644
index 0000000000..fdf189911f
--- /dev/null
+++ b/examples/nextjs-api-sales/db/docker-compose.yml
@@ -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:
diff --git a/examples/nextjs-api-sales/db/init.sql b/examples/nextjs-api-sales/db/init.sql
new file mode 100644
index 0000000000..f0a0c9d100
--- /dev/null
+++ b/examples/nextjs-api-sales/db/init.sql
@@ -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;
diff --git a/examples/nextjs-api-sales/definitions/index.ts b/examples/nextjs-api-sales/definitions/index.ts
new file mode 100644
index 0000000000..92cdd00571
--- /dev/null
+++ b/examples/nextjs-api-sales/definitions/index.ts
@@ -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"]
diff --git a/examples/nextjs-api-sales/definitions/openapi.ts b/examples/nextjs-api-sales/definitions/openapi.ts
new file mode 100644
index 0000000000..4f4ad45fc6
--- /dev/null
+++ b/examples/nextjs-api-sales/definitions/openapi.ts
@@ -0,0 +1,1125 @@
+/**
+ * This file was auto-generated by openapi-typescript.
+ * Do not make direct changes to the file.
+ */
+
+export interface paths {
+ "/applications": {
+ post: {
+ parameters: {
+ header: {
+ /** The ID of the app which this request is targeting. */
+ "x-budibase-app-id": components["parameters"]["appId"]
+ }
+ }
+ responses: {
+ /** Returns the created application. */
+ 200: {
+ content: {
+ "application/json": components["schemas"]["applicationOutput"]
+ }
+ }
+ }
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["application"]
+ }
+ }
+ }
+ }
+ "/applications/{appId}": {
+ get: {
+ parameters: {
+ path: {
+ /** The ID of the app which this request is targeting. */
+ appId: components["parameters"]["appIdUrl"]
+ }
+ }
+ responses: {
+ /** Returns the retrieved application. */
+ 200: {
+ content: {
+ "application/json": components["schemas"]["applicationOutput"]
+ }
+ }
+ }
+ }
+ put: {
+ parameters: {
+ path: {
+ /** The ID of the app which this request is targeting. */
+ appId: components["parameters"]["appIdUrl"]
+ }
+ }
+ responses: {
+ /** Returns the updated application. */
+ 200: {
+ content: {
+ "application/json": components["schemas"]["applicationOutput"]
+ }
+ }
+ }
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["application"]
+ }
+ }
+ }
+ delete: {
+ parameters: {
+ path: {
+ /** The ID of the app which this request is targeting. */
+ appId: components["parameters"]["appIdUrl"]
+ }
+ }
+ responses: {
+ /** Returns the deleted application. */
+ 200: {
+ content: {
+ "application/json": components["schemas"]["applicationOutput"]
+ }
+ }
+ }
+ }
+ }
+ "/applications/search": {
+ /** Based on application properties (currently only name) search for applications. */
+ post: {
+ parameters: {
+ header: {
+ /** The ID of the app which this request is targeting. */
+ "x-budibase-app-id": components["parameters"]["appId"]
+ }
+ }
+ responses: {
+ /** Returns the applications that were found based on the search parameters. */
+ 200: {
+ content: {
+ "application/json": components["schemas"]["applicationSearch"]
+ }
+ }
+ }
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["nameSearch"]
+ }
+ }
+ }
+ }
+ "/queries/{queryId}": {
+ /** Queries which have been created within a Budibase app can be executed using this, */
+ post: {
+ parameters: {
+ path: {
+ /** The ID of the query which this request is targeting. */
+ queryId: components["parameters"]["queryId"]
+ }
+ header: {
+ /** The ID of the app which this request is targeting. */
+ "x-budibase-app-id": components["parameters"]["appId"]
+ }
+ }
+ responses: {
+ /** Returns the result of the query execution. */
+ 200: {
+ content: {
+ "application/json": components["schemas"]["executeQueryOutput"]
+ }
+ }
+ }
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["executeQuery"]
+ }
+ }
+ }
+ }
+ "/queries/search": {
+ /** Based on query properties (currently only name) search for queries. */
+ post: {
+ parameters: {
+ header: {
+ /** The ID of the app which this request is targeting. */
+ "x-budibase-app-id": components["parameters"]["appId"]
+ }
+ }
+ responses: {
+ /** Returns the queries found based on the search parameters. */
+ 200: {
+ content: {
+ "application/json": components["schemas"]["querySearch"]
+ }
+ }
+ }
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["nameSearch"]
+ }
+ }
+ }
+ }
+ "/tables/{tableId}/rows": {
+ /** Creates a row within the specified table. */
+ post: {
+ parameters: {
+ path: {
+ /** The ID of the table which this request is targeting. */
+ tableId: components["parameters"]["tableId"]
+ }
+ header: {
+ /** The ID of the app which this request is targeting. */
+ "x-budibase-app-id": components["parameters"]["appId"]
+ }
+ }
+ responses: {
+ /** Returns the created row, including the ID which has been generated for it. This can be found in the Budibase portal, viewed under the developer information. */
+ 200: {
+ content: {
+ "application/json": components["schemas"]["rowOutput"]
+ }
+ }
+ }
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["row"]
+ }
+ }
+ }
+ }
+ "/tables/{tableId}/rows/{rowId}": {
+ /** This gets a single row, it will be enriched with the full related rows, rather than the squashed "primaryDisplay" format returned by the search endpoint. */
+ get: {
+ parameters: {
+ path: {
+ /** The ID of the table which this request is targeting. */
+ tableId: components["parameters"]["tableId"]
+ /** The ID of the row which this request is targeting. */
+ rowId: components["parameters"]["rowId"]
+ }
+ header: {
+ /** The ID of the app which this request is targeting. */
+ "x-budibase-app-id": components["parameters"]["appId"]
+ }
+ }
+ responses: {
+ /** Returns the retrieved row. */
+ 200: {
+ content: {
+ "application/json": components["schemas"]["rowOutput"]
+ }
+ }
+ }
+ }
+ /** Updates a row within the specified table. */
+ put: {
+ parameters: {
+ path: {
+ /** The ID of the table which this request is targeting. */
+ tableId: components["parameters"]["tableId"]
+ /** The ID of the row which this request is targeting. */
+ rowId: components["parameters"]["rowId"]
+ }
+ header: {
+ /** The ID of the app which this request is targeting. */
+ "x-budibase-app-id": components["parameters"]["appId"]
+ }
+ }
+ responses: {
+ /** Returns the created row, including the ID which has been generated for it. */
+ 200: {
+ content: {
+ "application/json": components["schemas"]["rowOutput"]
+ }
+ }
+ }
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["row"]
+ }
+ }
+ }
+ /** Deletes a row within the specified table. */
+ delete: {
+ parameters: {
+ path: {
+ /** The ID of the table which this request is targeting. */
+ tableId: components["parameters"]["tableId"]
+ /** The ID of the row which this request is targeting. */
+ rowId: components["parameters"]["rowId"]
+ }
+ header: {
+ /** The ID of the app which this request is targeting. */
+ "x-budibase-app-id": components["parameters"]["appId"]
+ }
+ }
+ responses: {
+ /** Returns the deleted row, including the ID which has been generated for it. */
+ 200: {
+ content: {
+ "application/json": components["schemas"]["rowOutput"]
+ }
+ }
+ }
+ }
+ }
+ "/tables/{tableId}/rows/search": {
+ post: {
+ parameters: {
+ path: {
+ /** The ID of the table which this request is targeting. */
+ tableId: components["parameters"]["tableId"]
+ }
+ header: {
+ /** The ID of the app which this request is targeting. */
+ "x-budibase-app-id": components["parameters"]["appId"]
+ }
+ }
+ responses: {
+ /** The response will contain an array of rows that match the search parameters. */
+ 200: {
+ content: {
+ "application/json": components["schemas"]["searchOutput"]
+ }
+ }
+ }
+ requestBody: {
+ content: {
+ "application/json": {
+ query: {
+ /**
+ * @description A map of field name to the string to search for, this will look for rows that have a value starting with the string value.
+ * @example [object Object]
+ */
+ string?: { [key: string]: string }
+ /** @description A fuzzy search, only supported by internal tables. */
+ fuzzy?: { [key: string]: unknown }
+ /**
+ * @description Searches within a range, the format of this must be columnName -> [low, high].
+ * @example [object Object]
+ */
+ range?: { [key: string]: unknown }
+ /** @description Searches for rows that have a column value that is exactly the value set. */
+ equal?: { [key: string]: unknown }
+ /** @description Searches for any row which does not contain the specified column value. */
+ notEqual?: { [key: string]: unknown }
+ /**
+ * @description Searches for rows which do not contain the specified column. The object should simply contain keys of the column names, these can map to any value.
+ * @example [object Object]
+ */
+ empty?: { [key: string]: unknown }
+ /** @description Searches for rows which have the specified column. */
+ notEmpty?: { [key: string]: unknown }
+ /** @description Searches for rows which have a column value that is any of the specified values. The format of this must be columnName -> [value1, value2]. */
+ oneOf?: { [key: string]: unknown }
+ }
+ /** @description Enables pagination, by default this is disabled. */
+ paginate?: boolean
+ /** @description If retrieving another page, the bookmark from the previous request must be supplied. */
+ bookmark?: string | number
+ /** @description The maximum number of rows to return, useful when paginating, for internal tables this will be limited to 1000, for SQL tables it will be 5000. */
+ limit?: number
+ /** @description A set of parameters describing the sort behaviour of the search. */
+ sort?: {
+ /**
+ * @description The order of the sort, by default this is ascending.
+ * @enum {string}
+ */
+ order?: "ascending" | "descending"
+ /** @description The name of the column by which the rows will be sorted. */
+ column?: string
+ /**
+ * @description Defines whether the column should be treated as a string or as numbers when sorting.
+ * @enum {string}
+ */
+ type?: "string" | "number"
+ }
+ }
+ }
+ }
+ }
+ }
+ "/tables": {
+ /** Create a table, this could be internal or external. */
+ post: {
+ parameters: {
+ header: {
+ /** The ID of the app which this request is targeting. */
+ "x-budibase-app-id": components["parameters"]["appId"]
+ }
+ }
+ responses: {
+ /** Returns the created table, including the ID which has been generated for it. This can be internal or external data sources. */
+ 200: {
+ content: {
+ "application/json": components["schemas"]["tableOutput"]
+ }
+ }
+ }
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["table"]
+ }
+ }
+ }
+ }
+ "/tables/{tableId}": {
+ /** Lookup a table, this could be internal or external. */
+ get: {
+ parameters: {
+ path: {
+ /** The ID of the table which this request is targeting. */
+ tableId: components["parameters"]["tableId"]
+ }
+ header: {
+ /** The ID of the app which this request is targeting. */
+ "x-budibase-app-id": components["parameters"]["appId"]
+ }
+ }
+ responses: {
+ /** Returns the retrieved table. */
+ 200: {
+ content: {
+ "application/json": components["schemas"]["tableOutput"]
+ }
+ }
+ }
+ }
+ /** Update a table, this could be internal or external. */
+ put: {
+ parameters: {
+ path: {
+ /** The ID of the table which this request is targeting. */
+ tableId: components["parameters"]["tableId"]
+ }
+ header: {
+ /** The ID of the app which this request is targeting. */
+ "x-budibase-app-id": components["parameters"]["appId"]
+ }
+ }
+ responses: {
+ /** Returns the updated table. */
+ 200: {
+ content: {
+ "application/json": components["schemas"]["tableOutput"]
+ }
+ }
+ }
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["table"]
+ }
+ }
+ }
+ /** Delete a table, this could be internal or external. */
+ delete: {
+ parameters: {
+ path: {
+ /** The ID of the table which this request is targeting. */
+ tableId: components["parameters"]["tableId"]
+ }
+ header: {
+ /** The ID of the app which this request is targeting. */
+ "x-budibase-app-id": components["parameters"]["appId"]
+ }
+ }
+ responses: {
+ /** Returns the deleted table. */
+ 200: {
+ content: {
+ "application/json": components["schemas"]["tableOutput"]
+ }
+ }
+ }
+ }
+ }
+ "/tables/search": {
+ /** Based on table properties (currently only name) search for tables. This could be an internal or an external table. */
+ post: {
+ parameters: {
+ header: {
+ /** The ID of the app which this request is targeting. */
+ "x-budibase-app-id": components["parameters"]["appId"]
+ }
+ }
+ responses: {
+ /** Returns the found tables, based on the search parameters. */
+ 200: {
+ content: {
+ "application/json": components["schemas"]["tableSearch"]
+ }
+ }
+ }
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["nameSearch"]
+ }
+ }
+ }
+ }
+ "/users": {
+ post: {
+ responses: {
+ /** Returns the created user. */
+ 200: {
+ content: {
+ "application/json": components["schemas"]["userOutput"]
+ }
+ }
+ }
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["user"]
+ }
+ }
+ }
+ }
+ "/users/{userId}": {
+ get: {
+ parameters: {
+ path: {
+ /** The ID of the user which this request is targeting. */
+ userId: components["parameters"]["userId"]
+ }
+ }
+ responses: {
+ /** Returns the retrieved user. */
+ 200: {
+ content: {
+ "application/json": components["schemas"]["userOutput"]
+ }
+ }
+ }
+ }
+ put: {
+ parameters: {
+ path: {
+ /** The ID of the user which this request is targeting. */
+ userId: components["parameters"]["userId"]
+ }
+ }
+ responses: {
+ /** Returns the updated user. */
+ 200: {
+ content: {
+ "application/json": components["schemas"]["userOutput"]
+ }
+ }
+ }
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["user"]
+ }
+ }
+ }
+ delete: {
+ parameters: {
+ path: {
+ /** The ID of the user which this request is targeting. */
+ userId: components["parameters"]["userId"]
+ }
+ }
+ responses: {
+ /** Returns the deleted user. */
+ 200: {
+ content: {
+ "application/json": components["schemas"]["userOutput"]
+ }
+ }
+ }
+ }
+ }
+ "/users/search": {
+ /** Based on user properties (currently only name) search for users. */
+ post: {
+ responses: {
+ /** Returns the found users based on search parameters. */
+ 200: {
+ content: {
+ "application/json": components["schemas"]["userSearch"]
+ }
+ }
+ }
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["nameSearch"]
+ }
+ }
+ }
+ }
+}
+
+export interface components {
+ schemas: {
+ application: {
+ /** @description The name of the app. */
+ name: string
+ /** @description The URL by which the app is accessed, this must be URL encoded. */
+ url: string
+ }
+ applicationOutput: {
+ data: {
+ /** @description The name of the app. */
+ name: string
+ /** @description The URL by which the app is accessed, this must be URL encoded. */
+ url: string
+ /** @description The ID of the app. */
+ _id: string
+ /**
+ * @description The status of the app, stating it if is the development or published version.
+ * @enum {string}
+ */
+ status: "development" | "published"
+ /** @description States when the app was created, will be constant. Stored in ISO format. */
+ createdAt: string
+ /** @description States the last time the app was updated - stored in ISO format. */
+ updatedAt: string
+ /** @description States the version of the Budibase client this app is currently based on. */
+ version: string
+ /** @description In a multi-tenant environment this will state the tenant this app is within. */
+ tenantId?: string
+ /** @description The user this app is currently being built by. */
+ lockedBy?: { [key: string]: unknown }
+ }
+ }
+ applicationSearch: {
+ data: {
+ /** @description The name of the app. */
+ name: string
+ /** @description The URL by which the app is accessed, this must be URL encoded. */
+ url: string
+ /** @description The ID of the app. */
+ _id: string
+ /**
+ * @description The status of the app, stating it if is the development or published version.
+ * @enum {string}
+ */
+ status: "development" | "published"
+ /** @description States when the app was created, will be constant. Stored in ISO format. */
+ createdAt: string
+ /** @description States the last time the app was updated - stored in ISO format. */
+ updatedAt: string
+ /** @description States the version of the Budibase client this app is currently based on. */
+ version: string
+ /** @description In a multi-tenant environment this will state the tenant this app is within. */
+ tenantId?: string
+ /** @description The user this app is currently being built by. */
+ lockedBy?: { [key: string]: unknown }
+ }[]
+ }
+ /** @description The row to be created/updated, based on the table schema. */
+ row: { [key: string]: unknown }
+ searchOutput: {
+ /** @description An array of rows, these will each contain an _id field which can be used to update or delete them. */
+ data: { [key: string]: unknown }[]
+ /** @description If pagination in use, this should be provided. */
+ bookmark?: string | number
+ /** @description If pagination in use, this will determine if there is another page to fetch. */
+ hasNextPage?: boolean
+ }
+ rowOutput: {
+ /** @description The row to be created/updated, based on the table schema. */
+ data: {
+ /** @description The ID of the row. */
+ _id: string
+ /** @description The ID of the table this row comes from. */
+ tableId: string
+ } & { [key: string]: unknown }
+ }
+ /** @description The table to be created/updated. */
+ table: {
+ /** @description The name of the table. */
+ name: string
+ /** @description The name of the column which should be used in relationship tags when relating to this table. */
+ primaryDisplay?: string
+ schema: {
+ [key: string]:
+ | {
+ /**
+ * @description A relationship column.
+ * @enum {string}
+ */
+ type?: "link"
+ /** @description A constraint can be applied to the column which will be validated against when a row is saved. */
+ constraints?: {
+ /** @enum {string} */
+ type?: "string" | "number" | "object" | "boolean"
+ /** @description Defines whether the column is required or not. */
+ presence?: boolean
+ }
+ /** @description The name of the column. */
+ name?: string
+ /** @description Defines whether the column is automatically generated. */
+ autocolumn?: boolean
+ /** @description The name of the column which a relationship column is related to in another table. */
+ fieldName?: string
+ /** @description The ID of the table which a relationship column is related to. */
+ tableId?: string
+ /**
+ * @description Defines the type of relationship that this column will be used for.
+ * @enum {string}
+ */
+ relationshipType?: "one-to-many" | "many-to-one" | "many-to-many"
+ /** @description When using a SQL table that contains many to many relationships this defines the table the relationships are linked through. */
+ through?: string
+ /** @description When using a SQL table that contains a one to many relationship this defines the foreign key. */
+ foreignKey?: string
+ /** @description When using a SQL table that utilises a through table, this defines the primary key in the through table for this table. */
+ throughFrom?: 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. */
+ throughTo?: string
+ }
+ | {
+ /**
+ * @description A formula column.
+ * @enum {string}
+ */
+ type?: "formula"
+ /** @description A constraint can be applied to the column which will be validated against when a row is saved. */
+ constraints?: {
+ /** @enum {string} */
+ type?: "string" | "number" | "object" | "boolean"
+ /** @description Defines whether the column is required or not. */
+ presence?: boolean
+ }
+ /** @description The name of the column. */
+ name?: string
+ /** @description Defines whether the column is automatically generated. */
+ autocolumn?: boolean
+ /** @description Defines a Handlebars or JavaScript formula to use, note that Javascript formulas are expected to be provided in the base64 format. */
+ formula?: string
+ /**
+ * @description Defines whether this is a static or dynamic formula.
+ * @enum {string}
+ */
+ formulaType?: "static" | "dynamic"
+ }
+ | {
+ /**
+ * @description Defines the type of the column, most explain themselves, a link column is a relationship.
+ * @enum {string}
+ */
+ type?:
+ | "string"
+ | "longform"
+ | "options"
+ | "number"
+ | "boolean"
+ | "array"
+ | "datetime"
+ | "attachment"
+ | "link"
+ | "formula"
+ | "auto"
+ | "json"
+ | "internal"
+ /** @description A constraint can be applied to the column which will be validated against when a row is saved. */
+ constraints?: {
+ /** @enum {string} */
+ type?: "string" | "number" | "object" | "boolean"
+ /** @description Defines whether the column is required or not. */
+ presence?: boolean
+ }
+ /** @description The name of the column. */
+ name?: string
+ /** @description Defines whether the column is automatically generated. */
+ autocolumn?: boolean
+ }
+ }
+ }
+ tableOutput: {
+ /** @description The table to be created/updated. */
+ data: {
+ /** @description The name of the table. */
+ name: string
+ /** @description The name of the column which should be used in relationship tags when relating to this table. */
+ primaryDisplay?: string
+ schema: {
+ [key: string]:
+ | {
+ /**
+ * @description A relationship column.
+ * @enum {string}
+ */
+ type?: "link"
+ /** @description A constraint can be applied to the column which will be validated against when a row is saved. */
+ constraints?: {
+ /** @enum {string} */
+ type?: "string" | "number" | "object" | "boolean"
+ /** @description Defines whether the column is required or not. */
+ presence?: boolean
+ }
+ /** @description The name of the column. */
+ name?: string
+ /** @description Defines whether the column is automatically generated. */
+ autocolumn?: boolean
+ /** @description The name of the column which a relationship column is related to in another table. */
+ fieldName?: string
+ /** @description The ID of the table which a relationship column is related to. */
+ tableId?: string
+ /**
+ * @description Defines the type of relationship that this column will be used for.
+ * @enum {string}
+ */
+ relationshipType?:
+ | "one-to-many"
+ | "many-to-one"
+ | "many-to-many"
+ /** @description When using a SQL table that contains many to many relationships this defines the table the relationships are linked through. */
+ through?: string
+ /** @description When using a SQL table that contains a one to many relationship this defines the foreign key. */
+ foreignKey?: string
+ /** @description When using a SQL table that utilises a through table, this defines the primary key in the through table for this table. */
+ throughFrom?: 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. */
+ throughTo?: string
+ }
+ | {
+ /**
+ * @description A formula column.
+ * @enum {string}
+ */
+ type?: "formula"
+ /** @description A constraint can be applied to the column which will be validated against when a row is saved. */
+ constraints?: {
+ /** @enum {string} */
+ type?: "string" | "number" | "object" | "boolean"
+ /** @description Defines whether the column is required or not. */
+ presence?: boolean
+ }
+ /** @description The name of the column. */
+ name?: string
+ /** @description Defines whether the column is automatically generated. */
+ autocolumn?: boolean
+ /** @description Defines a Handlebars or JavaScript formula to use, note that Javascript formulas are expected to be provided in the base64 format. */
+ formula?: string
+ /**
+ * @description Defines whether this is a static or dynamic formula.
+ * @enum {string}
+ */
+ formulaType?: "static" | "dynamic"
+ }
+ | {
+ /**
+ * @description Defines the type of the column, most explain themselves, a link column is a relationship.
+ * @enum {string}
+ */
+ type?:
+ | "string"
+ | "longform"
+ | "options"
+ | "number"
+ | "boolean"
+ | "array"
+ | "datetime"
+ | "attachment"
+ | "link"
+ | "formula"
+ | "auto"
+ | "json"
+ | "internal"
+ /** @description A constraint can be applied to the column which will be validated against when a row is saved. */
+ constraints?: {
+ /** @enum {string} */
+ type?: "string" | "number" | "object" | "boolean"
+ /** @description Defines whether the column is required or not. */
+ presence?: boolean
+ }
+ /** @description The name of the column. */
+ name?: string
+ /** @description Defines whether the column is automatically generated. */
+ autocolumn?: boolean
+ }
+ }
+ /** @description The ID of the table. */
+ _id: string
+ }
+ }
+ tableSearch: {
+ data: {
+ /** @description The name of the table. */
+ name: string
+ /** @description The name of the column which should be used in relationship tags when relating to this table. */
+ primaryDisplay?: string
+ schema: {
+ [key: string]:
+ | {
+ /**
+ * @description A relationship column.
+ * @enum {string}
+ */
+ type?: "link"
+ /** @description A constraint can be applied to the column which will be validated against when a row is saved. */
+ constraints?: {
+ /** @enum {string} */
+ type?: "string" | "number" | "object" | "boolean"
+ /** @description Defines whether the column is required or not. */
+ presence?: boolean
+ }
+ /** @description The name of the column. */
+ name?: string
+ /** @description Defines whether the column is automatically generated. */
+ autocolumn?: boolean
+ /** @description The name of the column which a relationship column is related to in another table. */
+ fieldName?: string
+ /** @description The ID of the table which a relationship column is related to. */
+ tableId?: string
+ /**
+ * @description Defines the type of relationship that this column will be used for.
+ * @enum {string}
+ */
+ relationshipType?:
+ | "one-to-many"
+ | "many-to-one"
+ | "many-to-many"
+ /** @description When using a SQL table that contains many to many relationships this defines the table the relationships are linked through. */
+ through?: string
+ /** @description When using a SQL table that contains a one to many relationship this defines the foreign key. */
+ foreignKey?: string
+ /** @description When using a SQL table that utilises a through table, this defines the primary key in the through table for this table. */
+ throughFrom?: 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. */
+ throughTo?: string
+ }
+ | {
+ /**
+ * @description A formula column.
+ * @enum {string}
+ */
+ type?: "formula"
+ /** @description A constraint can be applied to the column which will be validated against when a row is saved. */
+ constraints?: {
+ /** @enum {string} */
+ type?: "string" | "number" | "object" | "boolean"
+ /** @description Defines whether the column is required or not. */
+ presence?: boolean
+ }
+ /** @description The name of the column. */
+ name?: string
+ /** @description Defines whether the column is automatically generated. */
+ autocolumn?: boolean
+ /** @description Defines a Handlebars or JavaScript formula to use, note that Javascript formulas are expected to be provided in the base64 format. */
+ formula?: string
+ /**
+ * @description Defines whether this is a static or dynamic formula.
+ * @enum {string}
+ */
+ formulaType?: "static" | "dynamic"
+ }
+ | {
+ /**
+ * @description Defines the type of the column, most explain themselves, a link column is a relationship.
+ * @enum {string}
+ */
+ type?:
+ | "string"
+ | "longform"
+ | "options"
+ | "number"
+ | "boolean"
+ | "array"
+ | "datetime"
+ | "attachment"
+ | "link"
+ | "formula"
+ | "auto"
+ | "json"
+ | "internal"
+ /** @description A constraint can be applied to the column which will be validated against when a row is saved. */
+ constraints?: {
+ /** @enum {string} */
+ type?: "string" | "number" | "object" | "boolean"
+ /** @description Defines whether the column is required or not. */
+ presence?: boolean
+ }
+ /** @description The name of the column. */
+ name?: string
+ /** @description Defines whether the column is automatically generated. */
+ autocolumn?: boolean
+ }
+ }
+ /** @description The ID of the table. */
+ _id: string
+ }[]
+ }
+ /** @description The query body must contain the required parameters for the query, this depends on query type, setup and bindings. */
+ executeQuery: { [key: string]: unknown }
+ executeQueryOutput: {
+ /** @description The data response from the query. */
+ data: { [key: string]: unknown }[]
+ /** @description Extra information that is not part of the main data, e.g. headers. */
+ extra?: {
+ /** @description If carrying out a REST request, this will contain the response headers. */
+ headers?: { [key: string]: unknown }
+ /** @description The raw query response, as a string. */
+ raw?: string
+ }
+ /** @description If pagination is supported, this will contain the bookmark/anchor information for it. */
+ pagination?: { [key: string]: unknown }
+ }
+ query: {
+ /** @description The ID of the query. */
+ _id: string
+ /** @description The ID of the data source the query belongs to. */
+ datasourceId?: string
+ /** @description The bindings which are required to perform this query. */
+ parameters?: string[]
+ /** @description The fields that are used to perform this query, e.g. the sql statement */
+ fields?: { [key: string]: unknown }
+ /**
+ * @description The verb that describes this query.
+ * @enum {undefined}
+ */
+ queryVerb?: "create" | "read" | "update" | "delete"
+ /** @description The name of the query. */
+ name: string
+ /** @description The schema of the data returned when the query is executed. */
+ schema: { [key: string]: unknown }
+ /** @description The JavaScript transformer function, applied after the query responds with data. */
+ transformer?: string
+ /** @description Whether the query has readable data. */
+ readable?: boolean
+ }
+ querySearch: {
+ data: {
+ /** @description The ID of the query. */
+ _id: string
+ /** @description The ID of the data source the query belongs to. */
+ datasourceId?: string
+ /** @description The bindings which are required to perform this query. */
+ parameters?: string[]
+ /** @description The fields that are used to perform this query, e.g. the sql statement */
+ fields?: { [key: string]: unknown }
+ /**
+ * @description The verb that describes this query.
+ * @enum {undefined}
+ */
+ queryVerb?: "create" | "read" | "update" | "delete"
+ /** @description The name of the query. */
+ name: string
+ /** @description The schema of the data returned when the query is executed. */
+ schema: { [key: string]: unknown }
+ /** @description The JavaScript transformer function, applied after the query responds with data. */
+ transformer?: string
+ /** @description Whether the query has readable data. */
+ readable?: boolean
+ }[]
+ }
+ user: {
+ /** @description The email address of the user, this must be unique. */
+ email: string
+ /** @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. */
+ password?: string
+ /**
+ * @description The status of the user, if they are active.
+ * @enum {string}
+ */
+ status?: "active"
+ /** @description The first name of the user */
+ firstName?: string
+ /** @description The last name of the user */
+ lastName?: string
+ /** @description If set to true forces the user to reset their password on first login. */
+ forceResetPassword?: boolean
+ /** @description Describes if the user is a builder user or not. */
+ builder?: {
+ /** @description If set to true the user will be able to build any app in the system. */
+ global?: boolean
+ }
+ /** @description Describes if the user is an admin user or not. */
+ admin?: {
+ /** @description If set to true the user will be able to administrate the system. */
+ global?: boolean
+ }
+ /** @description Contains the roles of the user per app (assuming they are not a builder user). */
+ roles: { [key: string]: string }
+ }
+ userOutput: {
+ data: {
+ /** @description The email address of the user, this must be unique. */
+ email: string
+ /** @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. */
+ password?: string
+ /**
+ * @description The status of the user, if they are active.
+ * @enum {string}
+ */
+ status?: "active"
+ /** @description The first name of the user */
+ firstName?: string
+ /** @description The last name of the user */
+ lastName?: string
+ /** @description If set to true forces the user to reset their password on first login. */
+ forceResetPassword?: boolean
+ /** @description Describes if the user is a builder user or not. */
+ builder?: {
+ /** @description If set to true the user will be able to build any app in the system. */
+ global?: boolean
+ }
+ /** @description Describes if the user is an admin user or not. */
+ admin?: {
+ /** @description If set to true the user will be able to administrate the system. */
+ global?: boolean
+ }
+ /** @description Contains the roles of the user per app (assuming they are not a builder user). */
+ roles: { [key: string]: string }
+ /** @description The ID of the user. */
+ _id: string
+ }
+ }
+ userSearch: {
+ data: {
+ /** @description The email address of the user, this must be unique. */
+ email: string
+ /** @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. */
+ password?: string
+ /**
+ * @description The status of the user, if they are active.
+ * @enum {string}
+ */
+ status?: "active"
+ /** @description The first name of the user */
+ firstName?: string
+ /** @description The last name of the user */
+ lastName?: string
+ /** @description If set to true forces the user to reset their password on first login. */
+ forceResetPassword?: boolean
+ /** @description Describes if the user is a builder user or not. */
+ builder?: {
+ /** @description If set to true the user will be able to build any app in the system. */
+ global?: boolean
+ }
+ /** @description Describes if the user is an admin user or not. */
+ admin?: {
+ /** @description If set to true the user will be able to administrate the system. */
+ global?: boolean
+ }
+ /** @description Contains the roles of the user per app (assuming they are not a builder user). */
+ roles: { [key: string]: string }
+ /** @description The ID of the user. */
+ _id: string
+ }[]
+ }
+ nameSearch: {
+ /** @description The name to be used when searching - this will be used in a case insensitive starts with match. */
+ name: string
+ }
+ }
+ parameters: {
+ /** @description The ID of the table which this request is targeting. */
+ tableId: string
+ /** @description The ID of the row which this request is targeting. */
+ rowId: string
+ /** @description The ID of the app which this request is targeting. */
+ appId: string
+ /** @description The ID of the app which this request is targeting. */
+ appIdUrl: string
+ /** @description The ID of the query which this request is targeting. */
+ queryId: string
+ /** @description The ID of the user which this request is targeting. */
+ userId: string
+ }
+}
+
+export interface operations {}
+
+export interface external {}
diff --git a/examples/nextjs-api-sales/next-env.d.ts b/examples/nextjs-api-sales/next-env.d.ts
new file mode 100644
index 0000000000..4f11a03dc6
--- /dev/null
+++ b/examples/nextjs-api-sales/next-env.d.ts
@@ -0,0 +1,5 @@
+///
+///
+
+// NOTE: This file should not be edited
+// see https://nextjs.org/docs/basic-features/typescript for more information.
diff --git a/examples/nextjs-api-sales/next.config.js b/examples/nextjs-api-sales/next.config.js
new file mode 100644
index 0000000000..cfdafdaf5a
--- /dev/null
+++ b/examples/nextjs-api-sales/next.config.js
@@ -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
diff --git a/examples/nextjs-api-sales/package.json b/examples/nextjs-api-sales/package.json
new file mode 100644
index 0000000000..6d75c85f01
--- /dev/null
+++ b/examples/nextjs-api-sales/package.json
@@ -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"
+ }
+}
diff --git a/examples/nextjs-api-sales/pages/_app.tsx b/examples/nextjs-api-sales/pages/_app.tsx
new file mode 100644
index 0000000000..0669422fa4
--- /dev/null
+++ b/examples/nextjs-api-sales/pages/_app.tsx
@@ -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 (
+
+
+ BB NextJS Sales Example
+
+
+
+ )
+}
+
+export default MyApp
diff --git a/examples/nextjs-api-sales/pages/api/sales.ts b/examples/nextjs-api-sales/pages/api/sales.ts
new file mode 100644
index 0000000000..1652402158
--- /dev/null
+++ b/examples/nextjs-api-sales/pages/api/sales.ts
@@ -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)
+ }
+}
diff --git a/examples/nextjs-api-sales/pages/api/salespeople.ts b/examples/nextjs-api-sales/pages/api/salespeople.ts
new file mode 100644
index 0000000000..8ee43ab7c8
--- /dev/null
+++ b/examples/nextjs-api-sales/pages/api/salespeople.ts
@@ -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)
+ }
+}
diff --git a/examples/nextjs-api-sales/pages/index.tsx b/examples/nextjs-api-sales/pages/index.tsx
new file mode 100644
index 0000000000..eae1909ff4
--- /dev/null
+++ b/examples/nextjs-api-sales/pages/index.tsx
@@ -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 (
+