diff --git a/examples/nextjs-api-sales/definitions/index.ts b/examples/nextjs-api-sales/definitions/index.ts index 4eba86ca73..c84a893e81 100644 --- a/examples/nextjs-api-sales/definitions/index.ts +++ b/examples/nextjs-api-sales/definitions/index.ts @@ -1,7 +1,7 @@ import { components } from "./openapi" export type App = components["schemas"]["applicationOutput"]["data"] -export type AppSearch = { - data: App[] -} +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"] \ No newline at end of file diff --git a/examples/nextjs-api-sales/definitions/openapi.ts b/examples/nextjs-api-sales/definitions/openapi.ts index 71a9298040..4f4ad45fc6 100644 --- a/examples/nextjs-api-sales/definitions/openapi.ts +++ b/examples/nextjs-api-sales/definitions/openapi.ts @@ -95,9 +95,7 @@ export interface paths { /** Returns the applications that were found based on the search parameters. */ 200: { content: { - "application/json": { - data: components["schemas"]["application"][] - } + "application/json": components["schemas"]["applicationSearch"] } } } @@ -149,9 +147,7 @@ export interface paths { /** Returns the queries found based on the search parameters. */ 200: { content: { - "application/json": { - data: components["schemas"]["query"][] - } + "application/json": components["schemas"]["querySearch"] } } } @@ -449,9 +445,7 @@ export interface paths { /** Returns the found tables, based on the search parameters. */ 200: { content: { - "application/json": { - data: components["schemas"]["table"][] - } + "application/json": components["schemas"]["tableSearch"] } } } @@ -541,9 +535,7 @@ export interface paths { /** Returns the found users based on search parameters. */ 200: { content: { - "application/json": { - data: components["schemas"]["user"][] - } + "application/json": components["schemas"]["userSearch"] } } } @@ -589,6 +581,31 @@ export interface components { 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: { @@ -817,6 +834,113 @@ export interface components { _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: { @@ -855,6 +979,31 @@ export interface components { /** @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 @@ -917,6 +1066,39 @@ export interface components { _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 diff --git a/examples/nextjs-api-sales/next.config.js b/examples/nextjs-api-sales/next.config.js index 61b35090e4..335db3d3fe 100644 --- a/examples/nextjs-api-sales/next.config.js +++ b/examples/nextjs-api-sales/next.config.js @@ -6,8 +6,8 @@ const nextConfig = { includePaths: [join(__dirname, "styles")] }, serverRuntimeConfig: { - apiKey: "", - appName: "", + apiKey: "bf4d86af933b5ac0af0fdbe4bf7d89ff-f929752a1eeaafb00f4b5e3325097d51a44fe4b39f22ed857923409cc75414b379323a25ebfb4916", + appName: "sales", host: "http://localhost:10000" } } diff --git a/examples/nextjs-api-sales/pages/api/sales.ts b/examples/nextjs-api-sales/pages/api/sales.ts index 41b9c0d08e..0700711e88 100644 --- a/examples/nextjs-api-sales/pages/api/sales.ts +++ b/examples/nextjs-api-sales/pages/api/sales.ts @@ -1,13 +1,15 @@ import getConfig from "next/config" -import fetch from "node-fetch" -import { App, AppSearch, RowSearch } from "../../definitions" +import {App, AppSearch, Table, TableSearch} from "../../definitions" const { serverRuntimeConfig } = getConfig() const apiKey = serverRuntimeConfig["apiKey"] const appName = serverRuntimeConfig["appName"] const host = serverRuntimeConfig["host"] -async function makeCall(method: string, url: string, opts?: { body?: any, appId?: string } = {}): Promise { +let APP: App | null = null +let TABLES: { [key: string]: Table } = {} + +async function makeCall(method: string, url: string, opts?: { body?: any, appId?: string }): Promise { const fetchOpts: any = { method, headers: { @@ -18,49 +20,96 @@ async function makeCall(method: string, url: string, opts?: { body?: any, appId? fetchOpts.headers["x-budibase-app-id"] = opts.appId } if (opts?.body) { - fetchOpts.body = JSON.stringify(opts?.body) + fetchOpts.body = typeof opts.body !== "string" ? JSON.stringify(opts.body) : opts.body fetchOpts.headers["Content-Type"] = "application/json" } - const response = await fetch(`${host}/public/v1/${url}`, fetchOpts) - if (response.status === 200) { + const finalUrl = `${host}/api/public/v1/${url}` + const response = await fetch(finalUrl, fetchOpts) + if (response.ok) { return response.json() } else { - throw new Error(await response.text()) + const error = await response.text() + console.error("Budibase server error - ", error) + throw new Error(error) } } async function getApp(): Promise { + if (APP) { + return APP + } const apps: AppSearch = await makeCall("post", "applications/search", { body: { name: appName, } }) - if (!Array.isArray(apps?.data)) { - throw new Error("Fatal error, no apps found.") - } 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 } +async function findTable(appId: string, tableName: string): Promise { + 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 +} + 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: "ascending", + 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 = {} - if (req.method === "POST") { - response = await saveSale(req) - } else if (req.method === "GET") { - response = await getSales(req) - } else { - res.status(404) + 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) } - res.status(200).json(response) } \ No newline at end of file diff --git a/examples/nextjs-api-sales/pages/index.tsx b/examples/nextjs-api-sales/pages/index.tsx index 7b0ab6aebe..84debb998c 100644 --- a/examples/nextjs-api-sales/pages/index.tsx +++ b/examples/nextjs-api-sales/pages/index.tsx @@ -1,70 +1,61 @@ import type { NextPage } from "next" -import Head from "next/head" -import Image from "next/image" import styles from "../styles/home.module.css" +import { useState, useEffect } from "react" const Home: NextPage = () => { + const [sales, setSales] = useState([]) + const [currentPage, setCurrentPage] = useState(1) + + const getSales = async (page: Number = 1) => { + let url = "/api/sales" + if (page) { + url += `?page=${page}` + } + const response = await fetch(url) + if (!response.ok) { + throw new Error(await response.text()) + } + const sales = await response.json() + // @ts-ignore + setCurrentPage(page) + return setSales(sales.data) + } + + const saveSale = async () => { + const response = await fetch("/api/sales", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({}), + }) + if (!response.ok) { + throw new Error(await response.text()) + } + } + + const goToNextPage = async () => { + await getSales(currentPage + 1) + } + + const goToPrevPage = async () => { + if (currentPage > 1) { + await getSales(currentPage - 1) + } + } + + useEffect(() => { + getSales().catch(() => { + setSales([]) + }) + }, []) + return (
- - Create Next App - - - - -
-

- Welcome to Next.js! -

- -

- Get started by editing{" "} - pages/index.tsx -

- - -
- - +

Sales

+
{sales.map((sale: any) =>

{sale.sale_id}

)}
+ +
) }