Merge branch 'master' into BUDI-8270/validation-for-search-api
This commit is contained in:
commit
840e5e27df
|
@ -27,9 +27,8 @@
|
|||
"extends": "plugin:svelte/recommended",
|
||||
"parser": "svelte-eslint-parser",
|
||||
"parserOptions": {
|
||||
"parser": "@babel/eslint-parser",
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"ecmaVersion": 2019,
|
||||
"sourceType": "module",
|
||||
"allowImportExportEverywhere": true
|
||||
}
|
||||
},
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
name: ReadMe GitHub Action 🦉
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
rdme-openapi:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Use Node.js 20.x
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: yarn
|
||||
- run: yarn --frozen-lockfile
|
||||
|
||||
- name: update specs
|
||||
run: cd packages/server && yarn specs
|
||||
|
||||
- name: Run `openapi` command
|
||||
uses: readmeio/rdme@v8
|
||||
with:
|
||||
rdme: openapi specs/openapi.yaml --key=${{ secrets.README_API_KEY }} --id=6728a74f5918b50036c61841
|
|
@ -12,12 +12,12 @@ metadata:
|
|||
type: Opaque
|
||||
data:
|
||||
{{- if $existingSecret }}
|
||||
internalApiKey: {{ index $existingSecret.data "internalApiKey" }}
|
||||
jwtSecret: {{ index $existingSecret.data "jwtSecret" }}
|
||||
objectStoreAccess: {{ index $existingSecret.data "objectStoreAccess" }}
|
||||
objectStoreSecret: {{ index $existingSecret.data "objectStoreSecret" }}
|
||||
bbEncryptionKey: {{ index $existingSecret.data "bbEncryptionKey" }}
|
||||
apiEncryptionKey: {{ index $existingSecret.data "apiEncryptionKey" }}
|
||||
internalApiKey: {{ index $existingSecret.data "internalApiKey" | quote }}
|
||||
jwtSecret: {{ index $existingSecret.data "jwtSecret" | quote }}
|
||||
objectStoreAccess: {{ index $existingSecret.data "objectStoreAccess" | quote }}
|
||||
objectStoreSecret: {{ index $existingSecret.data "objectStoreSecret" | quote }}
|
||||
bbEncryptionKey: {{ index $existingSecret.data "bbEncryptionKey" | quote }}
|
||||
apiEncryptionKey: {{ index $existingSecret.data "apiEncryptionKey" | quote }}
|
||||
{{- else }}
|
||||
internalApiKey: {{ template "budibase.defaultsecret" .Values.globals.internalApiKey }}
|
||||
jwtSecret: {{ template "budibase.defaultsecret" .Values.globals.jwtSecret }}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
|
||||
"version": "3.2.11",
|
||||
"version": "3.2.14",
|
||||
"npmClient": "yarn",
|
||||
"concurrency": 20,
|
||||
"command": {
|
||||
|
|
|
@ -25,11 +25,12 @@
|
|||
"prettier": "2.8.8",
|
||||
"prettier-plugin-svelte": "^2.3.0",
|
||||
"proper-lockfile": "^4.1.2",
|
||||
"svelte": "^4.2.10",
|
||||
"svelte": "4.2.19",
|
||||
"svelte-eslint-parser": "^0.33.1",
|
||||
"typescript": "5.5.2",
|
||||
"typescript-eslint": "^7.3.1",
|
||||
"yargs": "^17.7.2"
|
||||
"yargs": "^17.7.2",
|
||||
"cross-spawn": "7.0.6"
|
||||
},
|
||||
"scripts": {
|
||||
"get-past-client-version": "node scripts/getPastClientVersion.js",
|
||||
|
|
|
@ -40,7 +40,7 @@
|
|||
"bcryptjs": "2.4.3",
|
||||
"bull": "4.10.1",
|
||||
"correlation-id": "4.0.0",
|
||||
"dd-trace": "5.23.0",
|
||||
"dd-trace": "5.26.0",
|
||||
"dotenv": "16.0.1",
|
||||
"google-auth-library": "^8.0.1",
|
||||
"google-spreadsheet": "npm:@budibase/google-spreadsheet@4.1.5",
|
||||
|
|
|
@ -190,7 +190,7 @@ export class DatabaseImpl implements Database {
|
|||
}
|
||||
}
|
||||
|
||||
private async performCall<T>(call: DBCallback<T>): Promise<any> {
|
||||
private async performCall<T>(call: DBCallback<T>): Promise<T> {
|
||||
const db = this.getDb()
|
||||
const fnc = await call(db)
|
||||
try {
|
||||
|
@ -467,7 +467,7 @@ export class DatabaseImpl implements Database {
|
|||
} catch (err: any) {
|
||||
// didn't exist, don't worry
|
||||
if (err.statusCode === 404) {
|
||||
return
|
||||
return { ok: true }
|
||||
} else {
|
||||
throw new CouchDBError(err.message, err)
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ export class DDInstrumentedDatabase implements Database {
|
|||
|
||||
exists(docId?: string): Promise<boolean> {
|
||||
return tracer.trace("db.exists", span => {
|
||||
span?.addTags({ db_name: this.name, doc_id: docId })
|
||||
span.addTags({ db_name: this.name, doc_id: docId })
|
||||
if (docId) {
|
||||
return this.db.exists(docId)
|
||||
}
|
||||
|
@ -37,15 +37,17 @@ export class DDInstrumentedDatabase implements Database {
|
|||
|
||||
get<T extends Document>(id?: string | undefined): Promise<T> {
|
||||
return tracer.trace("db.get", span => {
|
||||
span?.addTags({ db_name: this.name, doc_id: id })
|
||||
span.addTags({ db_name: this.name, doc_id: id })
|
||||
return this.db.get(id)
|
||||
})
|
||||
}
|
||||
|
||||
tryGet<T extends Document>(id?: string | undefined): Promise<T | undefined> {
|
||||
return tracer.trace("db.tryGet", span => {
|
||||
span?.addTags({ db_name: this.name, doc_id: id })
|
||||
return this.db.tryGet(id)
|
||||
return tracer.trace("db.tryGet", async span => {
|
||||
span.addTags({ db_name: this.name, doc_id: id })
|
||||
const doc = await this.db.tryGet<T>(id)
|
||||
span.addTags({ doc_found: doc !== undefined })
|
||||
return doc
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -53,13 +55,15 @@ export class DDInstrumentedDatabase implements Database {
|
|||
ids: string[],
|
||||
opts?: { allowMissing?: boolean | undefined } | undefined
|
||||
): Promise<T[]> {
|
||||
return tracer.trace("db.getMultiple", span => {
|
||||
span?.addTags({
|
||||
return tracer.trace("db.getMultiple", async span => {
|
||||
span.addTags({
|
||||
db_name: this.name,
|
||||
num_docs: ids.length,
|
||||
allow_missing: opts?.allowMissing,
|
||||
})
|
||||
return this.db.getMultiple(ids, opts)
|
||||
const docs = await this.db.getMultiple<T>(ids, opts)
|
||||
span.addTags({ num_docs_found: docs.length })
|
||||
return docs
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -69,12 +73,14 @@ export class DDInstrumentedDatabase implements Database {
|
|||
idOrDoc: string | Document,
|
||||
rev?: string
|
||||
): Promise<DocumentDestroyResponse> {
|
||||
return tracer.trace("db.remove", span => {
|
||||
span?.addTags({ db_name: this.name, doc_id: idOrDoc })
|
||||
return tracer.trace("db.remove", async span => {
|
||||
span.addTags({ db_name: this.name, doc_id: idOrDoc, rev })
|
||||
const isDocument = typeof idOrDoc === "object"
|
||||
const id = isDocument ? idOrDoc._id! : idOrDoc
|
||||
rev = isDocument ? idOrDoc._rev : rev
|
||||
return this.db.remove(id, rev)
|
||||
const resp = await this.db.remove(id, rev)
|
||||
span.addTags({ ok: resp.ok })
|
||||
return resp
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -83,7 +89,11 @@ export class DDInstrumentedDatabase implements Database {
|
|||
opts?: { silenceErrors?: boolean }
|
||||
): Promise<void> {
|
||||
return tracer.trace("db.bulkRemove", span => {
|
||||
span?.addTags({ db_name: this.name, num_docs: documents.length })
|
||||
span.addTags({
|
||||
db_name: this.name,
|
||||
num_docs: documents.length,
|
||||
silence_errors: opts?.silenceErrors,
|
||||
})
|
||||
return this.db.bulkRemove(documents, opts)
|
||||
})
|
||||
}
|
||||
|
@ -92,15 +102,21 @@ export class DDInstrumentedDatabase implements Database {
|
|||
document: AnyDocument,
|
||||
opts?: DatabasePutOpts | undefined
|
||||
): Promise<DocumentInsertResponse> {
|
||||
return tracer.trace("db.put", span => {
|
||||
span?.addTags({ db_name: this.name, doc_id: document._id })
|
||||
return this.db.put(document, opts)
|
||||
return tracer.trace("db.put", async span => {
|
||||
span.addTags({
|
||||
db_name: this.name,
|
||||
doc_id: document._id,
|
||||
force: opts?.force,
|
||||
})
|
||||
const resp = await this.db.put(document, opts)
|
||||
span.addTags({ ok: resp.ok })
|
||||
return resp
|
||||
})
|
||||
}
|
||||
|
||||
bulkDocs(documents: AnyDocument[]): Promise<DocumentBulkResponse[]> {
|
||||
return tracer.trace("db.bulkDocs", span => {
|
||||
span?.addTags({ db_name: this.name, num_docs: documents.length })
|
||||
span.addTags({ db_name: this.name, num_docs: documents.length })
|
||||
return this.db.bulkDocs(documents)
|
||||
})
|
||||
}
|
||||
|
@ -108,9 +124,15 @@ export class DDInstrumentedDatabase implements Database {
|
|||
allDocs<T extends Document | RowValue>(
|
||||
params: DatabaseQueryOpts
|
||||
): Promise<AllDocsResponse<T>> {
|
||||
return tracer.trace("db.allDocs", span => {
|
||||
span?.addTags({ db_name: this.name })
|
||||
return this.db.allDocs(params)
|
||||
return tracer.trace("db.allDocs", async span => {
|
||||
span.addTags({ db_name: this.name, ...params })
|
||||
const resp = await this.db.allDocs<T>(params)
|
||||
span.addTags({
|
||||
total_rows: resp.total_rows,
|
||||
rows_length: resp.rows.length,
|
||||
offset: resp.offset,
|
||||
})
|
||||
return resp
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -118,57 +140,75 @@ export class DDInstrumentedDatabase implements Database {
|
|||
viewName: string,
|
||||
params: DatabaseQueryOpts
|
||||
): Promise<AllDocsResponse<T>> {
|
||||
return tracer.trace("db.query", span => {
|
||||
span?.addTags({ db_name: this.name, view_name: viewName })
|
||||
return this.db.query(viewName, params)
|
||||
return tracer.trace("db.query", async span => {
|
||||
span.addTags({ db_name: this.name, view_name: viewName, ...params })
|
||||
const resp = await this.db.query<T>(viewName, params)
|
||||
span.addTags({
|
||||
total_rows: resp.total_rows,
|
||||
rows_length: resp.rows.length,
|
||||
offset: resp.offset,
|
||||
})
|
||||
return resp
|
||||
})
|
||||
}
|
||||
|
||||
destroy(): Promise<void | OkResponse> {
|
||||
return tracer.trace("db.destroy", span => {
|
||||
span?.addTags({ db_name: this.name })
|
||||
return this.db.destroy()
|
||||
destroy(): Promise<OkResponse> {
|
||||
return tracer.trace("db.destroy", async span => {
|
||||
span.addTags({ db_name: this.name })
|
||||
const resp = await this.db.destroy()
|
||||
span.addTags({ ok: resp.ok })
|
||||
return resp
|
||||
})
|
||||
}
|
||||
|
||||
compact(): Promise<void | OkResponse> {
|
||||
return tracer.trace("db.compact", span => {
|
||||
span?.addTags({ db_name: this.name })
|
||||
return this.db.compact()
|
||||
compact(): Promise<OkResponse> {
|
||||
return tracer.trace("db.compact", async span => {
|
||||
span.addTags({ db_name: this.name })
|
||||
const resp = await this.db.compact()
|
||||
span.addTags({ ok: resp.ok })
|
||||
return resp
|
||||
})
|
||||
}
|
||||
|
||||
dump(stream: Writable, opts?: DatabaseDumpOpts | undefined): Promise<any> {
|
||||
return tracer.trace("db.dump", span => {
|
||||
span?.addTags({ db_name: this.name })
|
||||
span.addTags({
|
||||
db_name: this.name,
|
||||
batch_limit: opts?.batch_limit,
|
||||
batch_size: opts?.batch_size,
|
||||
style: opts?.style,
|
||||
timeout: opts?.timeout,
|
||||
num_doc_ids: opts?.doc_ids?.length,
|
||||
view: opts?.view,
|
||||
})
|
||||
return this.db.dump(stream, opts)
|
||||
})
|
||||
}
|
||||
|
||||
load(...args: any[]): Promise<any> {
|
||||
return tracer.trace("db.load", span => {
|
||||
span?.addTags({ db_name: this.name })
|
||||
span.addTags({ db_name: this.name, num_args: args.length })
|
||||
return this.db.load(...args)
|
||||
})
|
||||
}
|
||||
|
||||
createIndex(...args: any[]): Promise<any> {
|
||||
return tracer.trace("db.createIndex", span => {
|
||||
span?.addTags({ db_name: this.name })
|
||||
span.addTags({ db_name: this.name, num_args: args.length })
|
||||
return this.db.createIndex(...args)
|
||||
})
|
||||
}
|
||||
|
||||
deleteIndex(...args: any[]): Promise<any> {
|
||||
return tracer.trace("db.deleteIndex", span => {
|
||||
span?.addTags({ db_name: this.name })
|
||||
span.addTags({ db_name: this.name, num_args: args.length })
|
||||
return this.db.deleteIndex(...args)
|
||||
})
|
||||
}
|
||||
|
||||
getIndexes(...args: any[]): Promise<any> {
|
||||
return tracer.trace("db.getIndexes", span => {
|
||||
span?.addTags({ db_name: this.name })
|
||||
span.addTags({ db_name: this.name, num_args: args.length })
|
||||
return this.db.getIndexes(...args)
|
||||
})
|
||||
}
|
||||
|
@ -177,22 +217,27 @@ export class DDInstrumentedDatabase implements Database {
|
|||
sql: string,
|
||||
parameters?: SqlQueryBinding
|
||||
): Promise<T[]> {
|
||||
return tracer.trace("db.sql", span => {
|
||||
span?.addTags({ db_name: this.name })
|
||||
return this.db.sql(sql, parameters)
|
||||
return tracer.trace("db.sql", async span => {
|
||||
span.addTags({ db_name: this.name, num_bindings: parameters?.length })
|
||||
const resp = await this.db.sql<T>(sql, parameters)
|
||||
span.addTags({ num_rows: resp.length })
|
||||
return resp
|
||||
})
|
||||
}
|
||||
|
||||
sqlPurgeDocument(docIds: string[] | string): Promise<void> {
|
||||
return tracer.trace("db.sqlPurgeDocument", span => {
|
||||
span?.addTags({ db_name: this.name })
|
||||
span.addTags({
|
||||
db_name: this.name,
|
||||
num_docs: Array.isArray(docIds) ? docIds.length : 1,
|
||||
})
|
||||
return this.db.sqlPurgeDocument(docIds)
|
||||
})
|
||||
}
|
||||
|
||||
sqlDiskCleanup(): Promise<void> {
|
||||
return tracer.trace("db.sqlDiskCleanup", span => {
|
||||
span?.addTags({ db_name: this.name })
|
||||
span.addTags({ db_name: this.name })
|
||||
return this.db.sqlDiskCleanup()
|
||||
})
|
||||
}
|
||||
|
|
|
@ -22,7 +22,6 @@ export function price(): PurchasedPrice {
|
|||
currency: "usd",
|
||||
duration: PriceDuration.MONTHLY,
|
||||
priceId: "price_123",
|
||||
dayPasses: undefined,
|
||||
isPerUser: true,
|
||||
}
|
||||
}
|
||||
|
@ -50,11 +49,6 @@ export function quotas(): Quotas {
|
|||
value: 1,
|
||||
triggers: [],
|
||||
},
|
||||
dayPasses: {
|
||||
name: "Queries",
|
||||
value: 1,
|
||||
triggers: [],
|
||||
},
|
||||
budibaseAICredits: {
|
||||
name: "Budibase AI Credits",
|
||||
value: 1,
|
||||
|
|
|
@ -15,7 +15,6 @@ export const usage = (users: number = 0, creators: number = 0): QuotaUsage => {
|
|||
monthly: {
|
||||
"01-2023": {
|
||||
automations: 0,
|
||||
dayPasses: 0,
|
||||
queries: 0,
|
||||
budibaseAICredits: 0,
|
||||
triggers: {},
|
||||
|
@ -45,14 +44,12 @@ export const usage = (users: number = 0, creators: number = 0): QuotaUsage => {
|
|||
},
|
||||
"02-2023": {
|
||||
automations: 0,
|
||||
dayPasses: 0,
|
||||
queries: 0,
|
||||
budibaseAICredits: 0,
|
||||
triggers: {},
|
||||
},
|
||||
current: {
|
||||
automations: 0,
|
||||
dayPasses: 0,
|
||||
queries: 0,
|
||||
budibaseAICredits: 0,
|
||||
triggers: {},
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
import AbsTooltip from "../Tooltip/AbsTooltip.svelte"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
|
||||
export let type
|
||||
export let type = undefined
|
||||
export let disabled = false
|
||||
export let size = "M"
|
||||
export let cta = false
|
||||
|
@ -16,8 +16,8 @@
|
|||
export let active = false
|
||||
export let tooltip = undefined
|
||||
export let newStyles = true
|
||||
export let id
|
||||
export let ref
|
||||
export let id = undefined
|
||||
export let ref = undefined
|
||||
export let reverse = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
|
|
@ -2,13 +2,6 @@
|
|||
import CoreDatePicker from "./DatePicker/DatePicker.svelte"
|
||||
import Icon from "../../Icon/Icon.svelte"
|
||||
|
||||
export let value = null
|
||||
export let disabled = false
|
||||
export let readonly = false
|
||||
export let error = null
|
||||
export let appendTo = undefined
|
||||
export let ignoreTimezones = false
|
||||
|
||||
let fromDate
|
||||
let toDate
|
||||
</script>
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
export let disabled = false
|
||||
export let updateOnChange = true
|
||||
export let quiet = false
|
||||
export let inputRef
|
||||
export let inputRef = undefined
|
||||
export let helpText = null
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
|
|
@ -17,18 +17,18 @@
|
|||
export let getOptionIcon = option => option?.icon
|
||||
export let getOptionColour = option => option?.colour
|
||||
export let useOptionIconImage = false
|
||||
export let isOptionEnabled
|
||||
export let isOptionEnabled = undefined
|
||||
export let quiet = false
|
||||
export let autoWidth = false
|
||||
export let sort = false
|
||||
export let tooltip = ""
|
||||
export let autocomplete = false
|
||||
export let customPopoverHeight
|
||||
export let align
|
||||
export let customPopoverHeight = undefined
|
||||
export let align = undefined
|
||||
export let footer = null
|
||||
export let tag = null
|
||||
export let helpText = null
|
||||
export let compare
|
||||
export let compare = undefined
|
||||
export let onOptionMouseenter = () => {}
|
||||
export let onOptionMouseleave = () => {}
|
||||
|
||||
|
|
|
@ -43,7 +43,7 @@
|
|||
export let showHeaderBorder = true
|
||||
export let placeholderText = "No rows found"
|
||||
export let snippets = []
|
||||
export let defaultSortColumn
|
||||
export let defaultSortColumn = undefined
|
||||
export let defaultSortOrder = "Ascending"
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<script>
|
||||
<script lang="ts">
|
||||
import "@spectrum-css/typography/dist/index-vars.css"
|
||||
|
||||
// Sizes
|
||||
export let size = "M"
|
||||
export let textAlign
|
||||
export let textAlign = undefined
|
||||
export let noPadding = false
|
||||
export let weight = "default" // light, heavy, default
|
||||
</script>
|
||||
|
|
|
@ -6,4 +6,4 @@ release/
|
|||
dist/
|
||||
routify
|
||||
.routify/
|
||||
svelte.config.js
|
||||
.rollup.cache
|
|
@ -4,13 +4,14 @@
|
|||
"license": "GPL-3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "routify -b && vite build --emptyOutDir",
|
||||
"build": "routify -b && NODE_OPTIONS=\"--max_old_space_size=4096\" vite build --emptyOutDir",
|
||||
"start": "routify -c rollup",
|
||||
"dev": "routify -c dev:vite",
|
||||
"dev:vite": "vite --host 0.0.0.0",
|
||||
"rollup": "rollup -c -w",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest"
|
||||
"test:watch": "vitest",
|
||||
"check:types": "yarn svelte-check"
|
||||
},
|
||||
"jest": {
|
||||
"globals": {
|
||||
|
@ -88,6 +89,7 @@
|
|||
"@babel/plugin-transform-runtime": "^7.13.10",
|
||||
"@babel/preset-env": "^7.13.12",
|
||||
"@rollup/plugin-replace": "^5.0.3",
|
||||
"@rollup/plugin-typescript": "8.3.0",
|
||||
"@roxi/routify": "2.18.12",
|
||||
"@sveltejs/vite-plugin-svelte": "1.4.0",
|
||||
"@testing-library/jest-dom": "6.4.2",
|
||||
|
@ -97,6 +99,7 @@
|
|||
"jest": "29.7.0",
|
||||
"jsdom": "^21.1.1",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
"svelte-check": "^4.1.0",
|
||||
"svelte-jester": "^1.3.2",
|
||||
"vite": "^4.5.0",
|
||||
"vite-plugin-static-copy": "^0.17.0",
|
||||
|
|
|
@ -8,7 +8,7 @@ import { get } from "svelte/store"
|
|||
import { auth, navigation } from "./stores/portal"
|
||||
|
||||
export const API = createAPIClient({
|
||||
attachHeaders: headers => {
|
||||
attachHeaders: (headers: Record<string, string>) => {
|
||||
// Attach app ID header from store
|
||||
let appId = get(appStore).appId
|
||||
if (appId) {
|
||||
|
@ -16,13 +16,13 @@ export const API = createAPIClient({
|
|||
}
|
||||
|
||||
// Add csrf token if authenticated
|
||||
const user = get(auth).user
|
||||
const user: any = get(auth).user
|
||||
if (user?.csrfToken) {
|
||||
headers["x-csrf-token"] = user.csrfToken
|
||||
}
|
||||
},
|
||||
|
||||
onError: error => {
|
||||
onError: (error: any) => {
|
||||
const { url, message, status, method, handled } = error || {}
|
||||
|
||||
// Log any errors that we haven't manually handled
|
||||
|
@ -45,14 +45,14 @@ export const API = createAPIClient({
|
|||
}
|
||||
}
|
||||
},
|
||||
onMigrationDetected: appId => {
|
||||
onMigrationDetected: (appId: string) => {
|
||||
const updatingUrl = `/builder/app/updating/${appId}`
|
||||
|
||||
if (window.location.pathname === updatingUrl) {
|
||||
return
|
||||
}
|
||||
|
||||
get(navigation).goto(
|
||||
get(navigation)?.goto(
|
||||
`${updatingUrl}?returnUrl=${encodeURIComponent(window.location.pathname)}`
|
||||
)
|
||||
},
|
|
@ -114,7 +114,7 @@
|
|||
$: schemaFields = search.getFields(
|
||||
$tables.list,
|
||||
Object.values(schema || {}),
|
||||
{ allowLinks: true }
|
||||
{ allowLinks: false }
|
||||
)
|
||||
$: queryLimit = tableId?.includes("datasource") ? "∞" : "1000"
|
||||
$: isTrigger = $memoBlock?.type === AutomationStepType.TRIGGER
|
||||
|
|
|
@ -1,81 +0,0 @@
|
|||
<script>
|
||||
import { Modal, ModalContent, Body, TooltipWrapper } from "@budibase/bbui"
|
||||
import { licensing, auth, admin } from "stores/portal"
|
||||
|
||||
export let onDismiss = () => {}
|
||||
export let onShow = () => {}
|
||||
|
||||
let dayPassModal
|
||||
|
||||
$: accountUrl = $admin.accountPortalUrl
|
||||
$: upgradeUrl = `${accountUrl}/portal/upgrade`
|
||||
|
||||
$: daysRemaining = $licensing.quotaResetDaysRemaining
|
||||
$: quotaResetDate = $licensing.quotaResetDate
|
||||
$: dayPassesUsed =
|
||||
$licensing.usageMetrics?.dayPasses > 100
|
||||
? 100
|
||||
: $licensing.usageMetrics?.dayPasses
|
||||
$: dayPassesTitle =
|
||||
dayPassesUsed >= 100
|
||||
? "You have run out of Day Passes"
|
||||
: "You are almost out of Day Passes"
|
||||
$: dayPassesBody =
|
||||
dayPassesUsed >= 100
|
||||
? "Upgrade your account to bring your apps back online."
|
||||
: "Upgrade your account to prevent your apps from going offline."
|
||||
|
||||
export function show() {
|
||||
dayPassModal.show()
|
||||
}
|
||||
|
||||
export function hide() {
|
||||
dayPassModal.hide()
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal bind:this={dayPassModal} on:show={onShow} on:hide={onDismiss}>
|
||||
{#if $auth.user.accountPortalAccess}
|
||||
<ModalContent
|
||||
title={dayPassesTitle}
|
||||
size="M"
|
||||
confirmText="Upgrade"
|
||||
onConfirm={() => {
|
||||
window.location.href = upgradeUrl
|
||||
}}
|
||||
>
|
||||
<Body>
|
||||
You have used <span class="daypass_percent">{dayPassesUsed}%</span> of
|
||||
your plans Day Passes with {daysRemaining} day{daysRemaining == 1
|
||||
? ""
|
||||
: "s"} remaining.
|
||||
<span class="tooltip">
|
||||
<TooltipWrapper tooltip={quotaResetDate} size="S" />
|
||||
</span>
|
||||
</Body>
|
||||
<Body>{dayPassesBody}</Body>
|
||||
</ModalContent>
|
||||
{:else}
|
||||
<ModalContent title={dayPassesTitle} size="M" showCancelButton={false}>
|
||||
<Body>
|
||||
You have used <span class="daypass_percent">{dayPassesUsed}%</span> of
|
||||
your plans Day Passes with {daysRemaining} day{daysRemaining == 1
|
||||
? ""
|
||||
: "s"} remaining.
|
||||
<span class="tooltip">
|
||||
<TooltipWrapper tooltip={quotaResetDate} size="S" />
|
||||
</span>
|
||||
</Body>
|
||||
<Body>Please contact your account holder to upgrade.</Body>
|
||||
</ModalContent>
|
||||
{/if}
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
.tooltip {
|
||||
display: inline-block;
|
||||
}
|
||||
.tooltip :global(.icon-container) {
|
||||
margin: 0px;
|
||||
}
|
||||
</style>
|
|
@ -1,7 +1,6 @@
|
|||
<script>
|
||||
import { licensing, auth, temporalStore } from "stores/portal"
|
||||
import { onMount } from "svelte"
|
||||
import DayPassWarningModal from "./DayPassWarningModal.svelte"
|
||||
import PaymentFailedModal from "./PaymentFailedModal.svelte"
|
||||
import AccountDowngradedModal from "./AccountDowngradedModal.svelte"
|
||||
import { ExpiringKeys } from "./constants"
|
||||
|
@ -12,7 +11,6 @@
|
|||
|
||||
let queuedBanners = []
|
||||
let queuedModals = []
|
||||
let dayPassModal
|
||||
let paymentFailedModal
|
||||
let accountDowngradeModal
|
||||
let userLoaded = false
|
||||
|
@ -26,18 +24,6 @@
|
|||
}
|
||||
|
||||
const dismissableModals = [
|
||||
{
|
||||
key: ExpiringKeys.LICENSING_DAYPASS_WARNING_MODAL,
|
||||
criteria: () => {
|
||||
return $licensing?.usageMetrics?.dayPasses >= 90
|
||||
},
|
||||
action: () => {
|
||||
dayPassModal.show()
|
||||
},
|
||||
cache: () => {
|
||||
defaultCacheFn(ExpiringKeys.LICENSING_DAYPASS_WARNING_MODAL)
|
||||
},
|
||||
},
|
||||
{
|
||||
key: ExpiringKeys.LICENSING_PAYMENT_FAILED,
|
||||
criteria: () => {
|
||||
|
@ -102,7 +88,6 @@
|
|||
})
|
||||
</script>
|
||||
|
||||
<DayPassWarningModal bind:this={dayPassModal} onDismiss={showNextModal} />
|
||||
<PaymentFailedModal bind:this={paymentFailedModal} onDismiss={showNextModal} />
|
||||
<AccountDowngradedModal
|
||||
bind:this={accountDowngradeModal}
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
export const ExpiringKeys = {
|
||||
LICENSING_DAYPASS_WARNING_MODAL: "licensing_daypass_warning_90_modal",
|
||||
LICENSING_DAYPASS_WARNING_BANNER: "licensing_daypass_warning_90_banner",
|
||||
LICENSING_PAYMENT_FAILED: "licensing_payment_failed",
|
||||
LICENSING_ACCOUNT_DOWNGRADED_MODAL: "licensing_account_downgraded_modal",
|
||||
LICENSING_APP_LIMIT_MODAL: "licensing_app_limit_modal",
|
||||
|
|
|
@ -84,45 +84,6 @@ const buildUsageInfoBanner = (
|
|||
}
|
||||
}
|
||||
|
||||
const buildDayPassBanner = () => {
|
||||
const appAuth = get(auth)
|
||||
const appLicensing = get(licensing)
|
||||
if (get(licensing)?.usageMetrics["dayPasses"] >= 100) {
|
||||
return {
|
||||
key: "max_dayPasses",
|
||||
type: BANNER_TYPES.NEGATIVE,
|
||||
criteria: () => {
|
||||
return true
|
||||
},
|
||||
message: `Your apps are currently offline. You have exceeded your plans limit for Day Passes. ${
|
||||
appAuth.user.accountPortalAccess
|
||||
? ""
|
||||
: "Please contact your account holder to upgrade."
|
||||
}`,
|
||||
...upgradeAction(),
|
||||
showCloseButton: false,
|
||||
}
|
||||
}
|
||||
|
||||
return buildUsageInfoBanner(
|
||||
"dayPasses",
|
||||
"Day Passes",
|
||||
ExpiringKeys.LICENSING_DAYPASS_WARNING_BANNER,
|
||||
90,
|
||||
`You have used ${
|
||||
appLicensing?.usageMetrics["dayPasses"]
|
||||
}% of your monthly usage of Day Passes with ${
|
||||
appLicensing?.quotaResetDaysRemaining
|
||||
} day${
|
||||
get(licensing).quotaResetDaysRemaining == 1 ? "" : "s"
|
||||
} remaining. All apps will be taken offline if this limit is reached. ${
|
||||
appAuth.user.accountPortalAccess
|
||||
? ""
|
||||
: "Please contact your account holder to upgrade."
|
||||
}`
|
||||
)
|
||||
}
|
||||
|
||||
const buildPaymentFailedBanner = () => {
|
||||
return {
|
||||
key: "payment_Failed",
|
||||
|
@ -166,7 +127,6 @@ const buildUsersAboveLimitBanner = EXPIRY_KEY => {
|
|||
export const getBanners = () => {
|
||||
return [
|
||||
buildPaymentFailedBanner(),
|
||||
buildDayPassBanner(ExpiringKeys.LICENSING_DAYPASS_WARNING_BANNER),
|
||||
buildUsageInfoBanner(
|
||||
"rows",
|
||||
"Rows",
|
||||
|
|
|
@ -68,7 +68,6 @@ export const OnboardingType = {
|
|||
|
||||
export const PlanModel = {
|
||||
PER_USER: "perUser",
|
||||
DAY_PASS: "dayPass",
|
||||
}
|
||||
|
||||
export const ChangelogURL = "https://docs.budibase.com/changelog"
|
||||
|
|
|
@ -1141,10 +1141,11 @@ export const buildFormSchema = (component, asset) => {
|
|||
const fieldSetting = settings.find(
|
||||
setting => setting.key === "field" && setting.type.startsWith("field/")
|
||||
)
|
||||
if (fieldSetting && component.field) {
|
||||
if (fieldSetting) {
|
||||
const type = fieldSetting.type.split("field/")[1]
|
||||
if (type) {
|
||||
schema[component.field] = { type }
|
||||
const key = component.field || component._instanceName
|
||||
if (type && key) {
|
||||
schema[key] = { type }
|
||||
}
|
||||
}
|
||||
component._children?.forEach(child => {
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
declare module "api" {
|
||||
const API: {
|
||||
getPlugins: () => Promise<any>
|
||||
createPlugin: (plugin: object) => Promise<any>
|
||||
uploadPlugin: (plugin: FormData) => Promise<any>
|
||||
deletePlugin: (id: string) => Promise<void>
|
||||
}
|
||||
}
|
|
@ -117,7 +117,4 @@
|
|||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -50,8 +50,6 @@
|
|||
border-radius: 4px;
|
||||
pointer-events: none;
|
||||
}
|
||||
.indicator.above {
|
||||
}
|
||||
.indicator.below {
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
|
|
@ -136,7 +136,7 @@
|
|||
</Body>
|
||||
</Layout>
|
||||
<Divider />
|
||||
{#if $licensing.usageMetrics?.dayPasses >= 100 || $licensing.errUserLimit}
|
||||
{#if $licensing.errUserLimit}
|
||||
<div>
|
||||
<Layout gap="S" justifyItems="center">
|
||||
<img class="spaceman" alt="spaceman" src={Spaceman} />
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
const upgradeUrl = `${$admin.accountPortalUrl}/portal/upgrade`
|
||||
const manageUrl = `${$admin.accountPortalUrl}/portal/billing`
|
||||
|
||||
const WARN_USAGE = ["Queries", "Automations", "Rows", "Day Passes", "Users"]
|
||||
const WARN_USAGE = ["Queries", "Automations", "Rows", "Users"]
|
||||
const oneDayInSeconds = 86400
|
||||
|
||||
const EXCLUDE_QUOTAS = {
|
||||
|
@ -36,9 +36,6 @@
|
|||
Users: license => {
|
||||
return license.plan.model !== PlanModel.PER_USER
|
||||
},
|
||||
"Day Passes": license => {
|
||||
return license.plan.model !== PlanModel.DAY_PASS
|
||||
},
|
||||
}
|
||||
|
||||
function excludeQuota(name) {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<script>
|
||||
<script lang="ts">
|
||||
import {
|
||||
Layout,
|
||||
Heading,
|
||||
|
@ -42,23 +42,25 @@
|
|||
{ column: "edit", component: EditPluginRenderer },
|
||||
]
|
||||
|
||||
let modal
|
||||
let searchTerm = ""
|
||||
let filter = "all"
|
||||
let modal: any
|
||||
let searchTerm: any = ""
|
||||
let filter: any = "all"
|
||||
let filterOptions = [
|
||||
{ label: "All plugins", value: "all" },
|
||||
{ label: "Components", value: "component" },
|
||||
]
|
||||
|
||||
const searchPlaceholder: any = "Search"
|
||||
|
||||
if (!$admin.cloud) {
|
||||
filterOptions.push({ label: "Datasources", value: "datasource" })
|
||||
}
|
||||
|
||||
$: filteredPlugins = $plugins
|
||||
.filter(plugin => {
|
||||
.filter((plugin: any) => {
|
||||
return filter === "all" || plugin.schema.type === filter
|
||||
})
|
||||
.filter(plugin => {
|
||||
.filter((plugin: any) => {
|
||||
return (
|
||||
!searchTerm ||
|
||||
plugin?.name?.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
|
@ -85,8 +87,8 @@
|
|||
<Button
|
||||
on:click={() =>
|
||||
window
|
||||
.open("https://github.com/Budibase/plugins", "_blank")
|
||||
.focus()}
|
||||
?.open("https://github.com/Budibase/plugins", "_blank")
|
||||
?.focus()}
|
||||
secondary
|
||||
>
|
||||
GitHub repo
|
||||
|
@ -98,12 +100,12 @@
|
|||
<div class="select">
|
||||
<Select
|
||||
bind:value={filter}
|
||||
placeholder={null}
|
||||
placeholder={undefined}
|
||||
options={filterOptions}
|
||||
autoWidth
|
||||
/>
|
||||
</div>
|
||||
<Search bind:value={searchTerm} placeholder="Search" />
|
||||
<Search bind:value={searchTerm} placeholder={searchPlaceholder} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
AutomationTriggerStepId,
|
||||
AutomationEventType,
|
||||
AutomationStepType,
|
||||
AutomationActionStepId,
|
||||
} from "@budibase/types"
|
||||
import { ActionStepID } from "constants/backend/automations"
|
||||
import { FIELDS } from "constants/backend"
|
||||
|
@ -466,9 +467,13 @@ const automationActions = store => ({
|
|||
.getPathSteps(block.pathTo, automation)
|
||||
.slice(0, -1)
|
||||
|
||||
// Current step will always be the last step of the path
|
||||
const currentBlock = store.actions
|
||||
.getPathSteps(block.pathTo, automation)
|
||||
.at(-1)
|
||||
|
||||
// Extract all outputs from all previous steps as available bindingsx§x
|
||||
let bindings = []
|
||||
|
||||
const addBinding = (name, value, icon, idx, isLoopBlock, bindingName) => {
|
||||
if (!name) return
|
||||
const runtimeBinding = determineRuntimeBinding(
|
||||
|
@ -519,9 +524,24 @@ const automationActions = store => ({
|
|||
runtimeName = `loop.${name}`
|
||||
} else if (idx === 0) {
|
||||
runtimeName = `trigger.${name}`
|
||||
} else {
|
||||
runtimeName = `steps.${pathSteps[idx]?.id}.${name}`
|
||||
} else if (
|
||||
currentBlock?.stepId === AutomationActionStepId.EXECUTE_SCRIPT
|
||||
) {
|
||||
const stepId = pathSteps[idx].id
|
||||
if (!stepId) {
|
||||
notifications.error("Error generating binding: Step ID not found.")
|
||||
return null
|
||||
}
|
||||
runtimeName = `steps["${stepId}"].${name}`
|
||||
} else {
|
||||
const stepId = pathSteps[idx].id
|
||||
if (!stepId) {
|
||||
notifications.error("Error generating binding: Step ID not found.")
|
||||
return null
|
||||
}
|
||||
runtimeName = `steps.${stepId}.${name}`
|
||||
}
|
||||
|
||||
return runtimeName
|
||||
}
|
||||
|
||||
|
@ -637,7 +657,6 @@ const automationActions = store => ({
|
|||
console.error("Loop block missing.")
|
||||
}
|
||||
}
|
||||
|
||||
Object.entries(schema).forEach(([name, value]) => {
|
||||
addBinding(name, value, icon, blockIdx, isLoopBlock, bindingName)
|
||||
})
|
||||
|
|
|
@ -1,13 +1,20 @@
|
|||
import { writable } from "svelte/store"
|
||||
|
||||
type GotoFuncType = (path: string) => void
|
||||
|
||||
interface Store {
|
||||
initialisated: boolean
|
||||
goto: GotoFuncType
|
||||
}
|
||||
|
||||
export function createNavigationStore() {
|
||||
const store = writable({
|
||||
const store = writable<Store>({
|
||||
initialisated: false,
|
||||
goto: undefined,
|
||||
goto: undefined as any,
|
||||
})
|
||||
const { set, subscribe } = store
|
||||
|
||||
const init = gotoFunc => {
|
||||
const init = (gotoFunc: GotoFuncType) => {
|
||||
if (typeof gotoFunc !== "function") {
|
||||
throw new Error(
|
||||
`gotoFunc must be a function, found a "${typeof gotoFunc}" instead`
|
|
@ -1,16 +1,21 @@
|
|||
import { writable } from "svelte/store"
|
||||
import { PluginSource } from "constants/index"
|
||||
|
||||
import { API } from "api"
|
||||
import { PluginSource } from "constants"
|
||||
|
||||
interface Plugin {
|
||||
_id: string
|
||||
}
|
||||
|
||||
export function createPluginsStore() {
|
||||
const { subscribe, set, update } = writable([])
|
||||
const { subscribe, set, update } = writable<Plugin[]>([])
|
||||
|
||||
async function load() {
|
||||
const plugins = await API.getPlugins()
|
||||
set(plugins)
|
||||
}
|
||||
|
||||
async function deletePlugin(pluginId) {
|
||||
async function deletePlugin(pluginId: string) {
|
||||
await API.deletePlugin(pluginId)
|
||||
update(state => {
|
||||
state = state.filter(existing => existing._id !== pluginId)
|
||||
|
@ -18,8 +23,8 @@ export function createPluginsStore() {
|
|||
})
|
||||
}
|
||||
|
||||
async function createPlugin(source, url, auth = null) {
|
||||
let pluginData = {
|
||||
async function createPlugin(source: string, url: string, auth = null) {
|
||||
let pluginData: any = {
|
||||
source,
|
||||
url,
|
||||
}
|
||||
|
@ -46,7 +51,7 @@ export function createPluginsStore() {
|
|||
})
|
||||
}
|
||||
|
||||
async function uploadPlugin(file) {
|
||||
async function uploadPlugin(file: File) {
|
||||
if (!file) {
|
||||
return
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
const { vitePreprocess } = require("@sveltejs/vite-plugin-svelte")
|
||||
|
||||
const config = {
|
||||
preprocess: vitePreprocess(),
|
||||
}
|
||||
|
||||
module.exports = config
|
|
@ -9,15 +9,9 @@
|
|||
"noImplicitAny": true,
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true,
|
||||
"incremental": true
|
||||
"incremental": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": [
|
||||
"./src/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"**/*.json",
|
||||
"**/*.spec.ts",
|
||||
"**/*.spec.js"
|
||||
]
|
||||
"include": ["./src/**/*"],
|
||||
"exclude": ["node_modules", "**/*.json", "**/*.spec.ts", "**/*.spec.js"]
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ import replace from "@rollup/plugin-replace"
|
|||
import { defineConfig, loadEnv } from "vite"
|
||||
import { viteStaticCopy } from "vite-plugin-static-copy"
|
||||
import path from "path"
|
||||
import typescript from "@rollup/plugin-typescript"
|
||||
|
||||
const ignoredWarnings = [
|
||||
"unused-export-let",
|
||||
|
@ -35,7 +36,7 @@ export default defineConfig(({ mode }) => {
|
|||
// Copy fonts to an additional path so that svelte's automatic
|
||||
// prefixing of the base URL path can still resolve assets
|
||||
copyFonts("builder/fonts"),
|
||||
]
|
||||
]
|
||||
|
||||
return {
|
||||
test: {
|
||||
|
@ -61,6 +62,7 @@ export default defineConfig(({ mode }) => {
|
|||
sourcemap: !isProduction,
|
||||
},
|
||||
plugins: [
|
||||
typescript({ outDir: "../server/builder/dist" }),
|
||||
svelte({
|
||||
hot: !isProduction,
|
||||
emitCss: true,
|
||||
|
|
|
@ -3096,7 +3096,6 @@
|
|||
"name": "Text Field",
|
||||
"icon": "Text",
|
||||
"styles": ["size"],
|
||||
"requiredAncestors": ["form"],
|
||||
"editable": true,
|
||||
"size": {
|
||||
"width": 400,
|
||||
|
@ -3106,8 +3105,7 @@
|
|||
{
|
||||
"type": "field/string",
|
||||
"label": "Field",
|
||||
"key": "field",
|
||||
"required": true
|
||||
"key": "field"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
|
@ -3226,13 +3224,22 @@
|
|||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"context": {
|
||||
"type": "static",
|
||||
"values": [
|
||||
{
|
||||
"label": "Value",
|
||||
"key": "value",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"numberfield": {
|
||||
"name": "Number Field",
|
||||
"icon": "123",
|
||||
"styles": ["size"],
|
||||
"requiredAncestors": ["form"],
|
||||
"editable": true,
|
||||
"size": {
|
||||
"width": 400,
|
||||
|
@ -3242,8 +3249,7 @@
|
|||
{
|
||||
"type": "field/number",
|
||||
"label": "Field",
|
||||
"key": "field",
|
||||
"required": true
|
||||
"key": "field"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
|
@ -3328,13 +3334,22 @@
|
|||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"context": {
|
||||
"type": "static",
|
||||
"values": [
|
||||
{
|
||||
"label": "Value",
|
||||
"key": "value",
|
||||
"type": "number"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"bigintfield": {
|
||||
"name": "BigInt Field",
|
||||
"icon": "TagBold",
|
||||
"styles": ["size"],
|
||||
"requiredAncestors": ["form"],
|
||||
"editable": true,
|
||||
"size": {
|
||||
"width": 400,
|
||||
|
@ -3344,8 +3359,7 @@
|
|||
{
|
||||
"type": "field/bigint",
|
||||
"label": "Field",
|
||||
"key": "field",
|
||||
"required": true
|
||||
"key": "field"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
|
@ -3414,13 +3428,22 @@
|
|||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"context": {
|
||||
"type": "static",
|
||||
"values": [
|
||||
{
|
||||
"label": "Value",
|
||||
"key": "value",
|
||||
"type": "number"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"passwordfield": {
|
||||
"name": "Password Field",
|
||||
"icon": "LockClosed",
|
||||
"styles": ["size"],
|
||||
"requiredAncestors": ["form"],
|
||||
"editable": true,
|
||||
"size": {
|
||||
"width": 400,
|
||||
|
@ -3430,8 +3453,7 @@
|
|||
{
|
||||
"type": "field/string",
|
||||
"label": "Field",
|
||||
"key": "field",
|
||||
"required": true
|
||||
"key": "field"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
|
@ -3500,13 +3522,22 @@
|
|||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"context": {
|
||||
"type": "static",
|
||||
"values": [
|
||||
{
|
||||
"label": "Value",
|
||||
"key": "value",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"optionsfield": {
|
||||
"name": "Options Picker",
|
||||
"icon": "Menu",
|
||||
"styles": ["size"],
|
||||
"requiredAncestors": ["form"],
|
||||
"editable": true,
|
||||
"size": {
|
||||
"width": 400,
|
||||
|
@ -3516,8 +3547,7 @@
|
|||
{
|
||||
"type": "field/options",
|
||||
"label": "Field",
|
||||
"key": "field",
|
||||
"required": true
|
||||
"key": "field"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
|
@ -3714,13 +3744,22 @@
|
|||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"context": {
|
||||
"type": "static",
|
||||
"values": [
|
||||
{
|
||||
"label": "Value",
|
||||
"key": "value",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"multifieldselect": {
|
||||
"name": "Multi-select Picker",
|
||||
"icon": "ViewList",
|
||||
"styles": ["size"],
|
||||
"requiredAncestors": ["form"],
|
||||
"editable": true,
|
||||
"size": {
|
||||
"width": 400,
|
||||
|
@ -3730,8 +3769,7 @@
|
|||
{
|
||||
"type": "field/array",
|
||||
"label": "Field",
|
||||
"key": "field",
|
||||
"required": true
|
||||
"key": "field"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
|
@ -3922,13 +3960,22 @@
|
|||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"context": {
|
||||
"type": "static",
|
||||
"values": [
|
||||
{
|
||||
"label": "Value",
|
||||
"key": "value",
|
||||
"type": "array"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"booleanfield": {
|
||||
"name": "Checkbox",
|
||||
"icon": "SelectBox",
|
||||
"editable": true,
|
||||
"requiredAncestors": ["form"],
|
||||
"size": {
|
||||
"width": 400,
|
||||
"height": 60
|
||||
|
@ -3937,8 +3984,7 @@
|
|||
{
|
||||
"type": "field/boolean",
|
||||
"label": "Field",
|
||||
"key": "field",
|
||||
"required": true
|
||||
"key": "field"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
|
@ -4047,13 +4093,22 @@
|
|||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"context": {
|
||||
"type": "static",
|
||||
"values": [
|
||||
{
|
||||
"label": "Value",
|
||||
"key": "value",
|
||||
"type": "boolean"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"longformfield": {
|
||||
"name": "Long Form Field",
|
||||
"icon": "TextAlignLeft",
|
||||
"styles": ["size"],
|
||||
"requiredAncestors": ["form"],
|
||||
"editable": true,
|
||||
"size": {
|
||||
"width": 400,
|
||||
|
@ -4063,8 +4118,7 @@
|
|||
{
|
||||
"type": "field/longform",
|
||||
"label": "Field",
|
||||
"key": "field",
|
||||
"required": true
|
||||
"key": "field"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
|
@ -4171,13 +4225,22 @@
|
|||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"context": {
|
||||
"type": "static",
|
||||
"values": [
|
||||
{
|
||||
"label": "Value",
|
||||
"key": "value",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"datetimefield": {
|
||||
"name": "Date Picker",
|
||||
"icon": "Date",
|
||||
"styles": ["size"],
|
||||
"requiredAncestors": ["form"],
|
||||
"editable": true,
|
||||
"size": {
|
||||
"width": 400,
|
||||
|
@ -4187,8 +4250,7 @@
|
|||
{
|
||||
"type": "field/datetime",
|
||||
"label": "Field",
|
||||
"key": "field",
|
||||
"required": true
|
||||
"key": "field"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
|
@ -4291,7 +4353,17 @@
|
|||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"context": {
|
||||
"type": "static",
|
||||
"values": [
|
||||
{
|
||||
"label": "Value",
|
||||
"key": "value",
|
||||
"type": "datetime"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"codescanner": {
|
||||
"name": "Barcode/QR Scanner",
|
||||
|
@ -4305,8 +4377,7 @@
|
|||
{
|
||||
"type": "field/barcodeqr",
|
||||
"label": "Field",
|
||||
"key": "field",
|
||||
"required": true
|
||||
"key": "field"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
|
@ -4451,7 +4522,17 @@
|
|||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"context": {
|
||||
"type": "static",
|
||||
"values": [
|
||||
{
|
||||
"label": "Value",
|
||||
"key": "value",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"signaturesinglefield": {
|
||||
"name": "Signature",
|
||||
|
@ -4924,7 +5005,6 @@
|
|||
"icon": "Brackets",
|
||||
"styles": ["size"],
|
||||
"editable": true,
|
||||
"requiredAncestors": ["form"],
|
||||
"size": {
|
||||
"width": 400,
|
||||
"height": 100
|
||||
|
@ -4933,8 +5013,7 @@
|
|||
{
|
||||
"type": "field/json",
|
||||
"label": "Field",
|
||||
"key": "field",
|
||||
"required": true
|
||||
"key": "field"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
|
@ -5014,7 +5093,17 @@
|
|||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"context": {
|
||||
"type": "static",
|
||||
"values": [
|
||||
{
|
||||
"label": "Value",
|
||||
"key": "value",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"s3upload": {
|
||||
"name": "S3 File Upload",
|
||||
|
@ -5029,8 +5118,7 @@
|
|||
{
|
||||
"type": "field/s3",
|
||||
"label": "Field",
|
||||
"key": "field",
|
||||
"required": true
|
||||
"key": "field"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
|
@ -5075,7 +5163,17 @@
|
|||
"label": "Validation",
|
||||
"key": "validation"
|
||||
}
|
||||
],
|
||||
"context": {
|
||||
"type": "static",
|
||||
"values": [
|
||||
{
|
||||
"label": "Value",
|
||||
"key": "value",
|
||||
"type": "array"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"dataprovider": {
|
||||
"name": "Data Provider",
|
||||
|
@ -7643,7 +7741,6 @@
|
|||
"name": "User List Field",
|
||||
"icon": "UserGroup",
|
||||
"styles": ["size"],
|
||||
"requiredAncestors": ["form"],
|
||||
"editable": true,
|
||||
"size": {
|
||||
"width": 400,
|
||||
|
@ -7653,8 +7750,7 @@
|
|||
{
|
||||
"type": "field/bb_reference",
|
||||
"label": "Field",
|
||||
"key": "field",
|
||||
"required": true
|
||||
"key": "field"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
|
@ -7744,14 +7840,23 @@
|
|||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"context": {
|
||||
"type": "static",
|
||||
"values": [
|
||||
{
|
||||
"label": "Value",
|
||||
"key": "value",
|
||||
"type": "array"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"bbreferencesinglefield": {
|
||||
"devComment": "As bb reference is only used for user subtype for now, we are using user for icon and labels",
|
||||
"name": "User Field",
|
||||
"icon": "User",
|
||||
"styles": ["size"],
|
||||
"requiredAncestors": ["form"],
|
||||
"editable": true,
|
||||
"size": {
|
||||
"width": 400,
|
||||
|
@ -7761,8 +7866,7 @@
|
|||
{
|
||||
"type": "field/bb_reference_single",
|
||||
"label": "Field",
|
||||
"key": "field",
|
||||
"required": true
|
||||
"key": "field"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
|
@ -7852,6 +7956,16 @@
|
|||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"context": {
|
||||
"type": "static",
|
||||
"values": [
|
||||
{
|
||||
"label": "Value",
|
||||
"key": "value",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,8 +15,8 @@
|
|||
"./manifest.json": "./manifest.json"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "rollup -c",
|
||||
"dev": "rollup -cw"
|
||||
"build": "vite build",
|
||||
"dev": "vite build --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/bbui": "0.0.0",
|
||||
|
@ -36,19 +36,9 @@
|
|||
"svelte-spa-router": "^4.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-alias": "^5.1.0",
|
||||
"@rollup/plugin-commonjs": "^25.0.7",
|
||||
"@rollup/plugin-image": "^3.0.3",
|
||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||
"postcss": "^8.4.35",
|
||||
"rollup": "^4.9.6",
|
||||
"rollup-plugin-json": "^4.0.0",
|
||||
"rollup-plugin-polyfill-node": "^0.13.0",
|
||||
"rollup-plugin-postcss": "^4.0.2",
|
||||
"rollup-plugin-svelte": "^7.1.6",
|
||||
"rollup-plugin-svg": "^2.0.0",
|
||||
"rollup-plugin-terser": "^7.0.2",
|
||||
"rollup-plugin-visualizer": "^5.12.0"
|
||||
"@sveltejs/vite-plugin-svelte": "1.4.0",
|
||||
"vite": "^4.5.0",
|
||||
"vite-plugin-css-injected-by-js": "3.5.2"
|
||||
},
|
||||
"resolutions": {
|
||||
"loader-utils": "1.4.1"
|
||||
|
|
|
@ -1,117 +0,0 @@
|
|||
import commonjs from "@rollup/plugin-commonjs"
|
||||
import resolve from "@rollup/plugin-node-resolve"
|
||||
import alias from "@rollup/plugin-alias"
|
||||
import svelte from "rollup-plugin-svelte"
|
||||
import { terser } from "rollup-plugin-terser"
|
||||
import postcss from "rollup-plugin-postcss"
|
||||
import svg from "rollup-plugin-svg"
|
||||
import image from "@rollup/plugin-image"
|
||||
import json from "rollup-plugin-json"
|
||||
import nodePolyfills from "rollup-plugin-polyfill-node"
|
||||
import path from "path"
|
||||
import { visualizer } from "rollup-plugin-visualizer"
|
||||
|
||||
const production = !process.env.ROLLUP_WATCH
|
||||
const ignoredWarnings = [
|
||||
"unused-export-let",
|
||||
"css-unused-selector",
|
||||
"module-script-reactive-declaration",
|
||||
"a11y-no-onchange",
|
||||
"a11y-click-events-have-key-events",
|
||||
]
|
||||
|
||||
const devPaths = production
|
||||
? []
|
||||
: [
|
||||
{
|
||||
find: "@budibase/shared-core",
|
||||
replacement: path.resolve("../shared-core/dist/index"),
|
||||
},
|
||||
{
|
||||
find: "@budibase/types",
|
||||
replacement: path.resolve("../types/dist/index"),
|
||||
},
|
||||
]
|
||||
|
||||
export default {
|
||||
input: "src/index.js",
|
||||
output: [
|
||||
{
|
||||
sourcemap: false,
|
||||
format: "iife",
|
||||
file: `./dist/budibase-client.js`,
|
||||
},
|
||||
],
|
||||
onwarn(warning, warn) {
|
||||
if (
|
||||
warning.code === "THIS_IS_UNDEFINED" ||
|
||||
warning.code === "CIRCULAR_DEPENDENCY" ||
|
||||
warning.code === "EVAL"
|
||||
) {
|
||||
return
|
||||
}
|
||||
warn(warning)
|
||||
},
|
||||
plugins: [
|
||||
alias({
|
||||
entries: [
|
||||
{
|
||||
find: "manifest.json",
|
||||
replacement: path.resolve("./manifest.json"),
|
||||
},
|
||||
{
|
||||
find: "api",
|
||||
replacement: path.resolve("./src/api"),
|
||||
},
|
||||
{
|
||||
find: "components",
|
||||
replacement: path.resolve("./src/components"),
|
||||
},
|
||||
{
|
||||
find: "stores",
|
||||
replacement: path.resolve("./src/stores"),
|
||||
},
|
||||
{
|
||||
find: "utils",
|
||||
replacement: path.resolve("./src/utils"),
|
||||
},
|
||||
{
|
||||
find: "constants",
|
||||
replacement: path.resolve("./src/constants"),
|
||||
},
|
||||
{
|
||||
find: "sdk",
|
||||
replacement: path.resolve("./src/sdk"),
|
||||
},
|
||||
...devPaths,
|
||||
],
|
||||
}),
|
||||
svelte({
|
||||
emitCss: true,
|
||||
onwarn: (warning, handler) => {
|
||||
// Ignore some warnings
|
||||
if (!ignoredWarnings.includes(warning.code)) {
|
||||
handler(warning)
|
||||
}
|
||||
},
|
||||
}),
|
||||
postcss(),
|
||||
commonjs(),
|
||||
nodePolyfills(),
|
||||
resolve({
|
||||
preferBuiltins: true,
|
||||
browser: true,
|
||||
dedupe: ["svelte", "svelte/internal"],
|
||||
}),
|
||||
svg(),
|
||||
image({
|
||||
exclude: "**/*.svg",
|
||||
}),
|
||||
json(),
|
||||
production && terser(),
|
||||
!production && visualizer(),
|
||||
],
|
||||
watch: {
|
||||
clearScreen: false,
|
||||
},
|
||||
}
|
|
@ -18,8 +18,6 @@
|
|||
export let palette
|
||||
export let c1, c2, c3, c4, c5
|
||||
|
||||
// Area specific props
|
||||
export let area
|
||||
export let stacked
|
||||
export let gradient
|
||||
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
<script>
|
||||
import Placeholder from "../Placeholder.svelte"
|
||||
import { getContext, onDestroy } from "svelte"
|
||||
import { writable } from "svelte/store"
|
||||
import { Icon } from "@budibase/bbui"
|
||||
import { memo } from "@budibase/frontend-core"
|
||||
import Placeholder from "../Placeholder.svelte"
|
||||
import InnerForm from "./InnerForm.svelte"
|
||||
|
||||
export let label
|
||||
export let field
|
||||
|
@ -20,26 +23,39 @@
|
|||
const formContext = getContext("form")
|
||||
const formStepContext = getContext("form-step")
|
||||
const fieldGroupContext = getContext("field-group")
|
||||
const { styleable, builderStore } = getContext("sdk")
|
||||
const { styleable, builderStore, Provider } = getContext("sdk")
|
||||
const component = getContext("component")
|
||||
|
||||
// Register field with form
|
||||
const formApi = formContext?.formApi
|
||||
const labelPos = fieldGroupContext?.labelPosition || "above"
|
||||
|
||||
let formField
|
||||
let touched = false
|
||||
let labelNode
|
||||
|
||||
$: formStep = formStepContext ? $formStepContext || 1 : 1
|
||||
$: formField = formApi?.registerField(
|
||||
field,
|
||||
// Memoize values required to register the field to avoid loops
|
||||
const formStep = formStepContext || writable(1)
|
||||
const fieldInfo = memo({
|
||||
field: field || $component.name,
|
||||
type,
|
||||
defaultValue,
|
||||
disabled,
|
||||
readonly,
|
||||
validation,
|
||||
formStep
|
||||
)
|
||||
formStep: $formStep || 1,
|
||||
})
|
||||
$: fieldInfo.set({
|
||||
field: field || $component.name,
|
||||
type,
|
||||
defaultValue,
|
||||
disabled,
|
||||
readonly,
|
||||
validation,
|
||||
formStep: $formStep || 1,
|
||||
})
|
||||
$: registerField($fieldInfo)
|
||||
|
||||
$: schemaType =
|
||||
fieldSchema?.type !== "formula" && fieldSchema?.type !== "bigint"
|
||||
? fieldSchema?.type
|
||||
|
@ -58,6 +74,18 @@
|
|||
// Determine label class from position
|
||||
$: labelClass = labelPos === "above" ? "" : `spectrum-FieldLabel--${labelPos}`
|
||||
|
||||
const registerField = info => {
|
||||
formField = formApi?.registerField(
|
||||
info.field,
|
||||
info.type,
|
||||
info.defaultValue,
|
||||
info.disabled,
|
||||
info.readonly,
|
||||
info.validation,
|
||||
info.formStep
|
||||
)
|
||||
}
|
||||
|
||||
const updateLabel = e => {
|
||||
if (touched) {
|
||||
builderStore.actions.updateProp("label", e.target.textContent)
|
||||
|
@ -71,14 +99,27 @@
|
|||
})
|
||||
</script>
|
||||
|
||||
<div
|
||||
<Provider data={{ value: fieldState?.value }}>
|
||||
{#if !formContext}
|
||||
<InnerForm
|
||||
{disabled}
|
||||
{readonly}
|
||||
currentStep={writable(1)}
|
||||
provideContext={false}
|
||||
>
|
||||
<svelte:self {...$$props} bind:fieldState bind:fieldApi bind:fieldSchema>
|
||||
<slot />
|
||||
</svelte:self>
|
||||
</InnerForm>
|
||||
{:else}
|
||||
<div
|
||||
class="spectrum-Form-item"
|
||||
class:span-2={span === 2}
|
||||
class:span-3={span === 3}
|
||||
class:span-6={span === 6 || !span}
|
||||
use:styleable={$component.styles}
|
||||
class:above={labelPos === "above"}
|
||||
>
|
||||
>
|
||||
{#key $component.editing}
|
||||
<label
|
||||
bind:this={labelNode}
|
||||
|
@ -94,9 +135,7 @@
|
|||
</label>
|
||||
{/key}
|
||||
<div class="spectrum-Form-itemField">
|
||||
{#if !formContext}
|
||||
<Placeholder text="Form components need to be wrapped in a form" />
|
||||
{:else if !fieldState}
|
||||
{#if !fieldState}
|
||||
<Placeholder />
|
||||
{:else if schemaType && schemaType !== type && !["options", "longform"].includes(type)}
|
||||
<Placeholder
|
||||
|
@ -116,7 +155,9 @@
|
|||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</Provider>
|
||||
|
||||
<style>
|
||||
:global(.form-block .spectrum-Form-item.span-2) {
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
import { writable } from "svelte/store"
|
||||
|
||||
export let dataSource
|
||||
export let theme
|
||||
export let size
|
||||
export let disabled = false
|
||||
export let readonly = false
|
||||
|
@ -113,11 +112,9 @@
|
|||
{#key resetKey}
|
||||
<InnerForm
|
||||
{dataSource}
|
||||
{theme}
|
||||
{size}
|
||||
{disabled}
|
||||
{readonly}
|
||||
{actionType}
|
||||
{schema}
|
||||
{definition}
|
||||
{initialValues}
|
||||
|
|
|
@ -14,6 +14,10 @@
|
|||
export let disableSchemaValidation = false
|
||||
export let editAutoColumns = false
|
||||
|
||||
// For internal use only, to disable context when being used with standalone
|
||||
// fields
|
||||
export let provideContext = true
|
||||
|
||||
// We export this store so that when we remount the inner form we can still
|
||||
// persist what step we're on
|
||||
export let currentStep
|
||||
|
@ -442,8 +446,14 @@
|
|||
]
|
||||
</script>
|
||||
|
||||
<Provider {actions} data={dataContext}>
|
||||
{#if provideContext}
|
||||
<Provider {actions} data={dataContext}>
|
||||
<div use:styleable={$component.styles} class={size}>
|
||||
<slot />
|
||||
</div>
|
||||
</Provider>
|
||||
</Provider>
|
||||
{:else}
|
||||
<div use:styleable={$component.styles} class={size}>
|
||||
<slot />
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
@ -10,7 +10,7 @@ import {
|
|||
eventStore,
|
||||
hoverStore,
|
||||
} from "./stores"
|
||||
import loadSpectrumIcons from "@budibase/bbui/spectrum-icons-rollup.js"
|
||||
import loadSpectrumIcons from "@budibase/bbui/spectrum-icons-vite.js"
|
||||
import { get } from "svelte/store"
|
||||
import { initWebsocket } from "./websocket.js"
|
||||
|
||||
|
|
|
@ -3,6 +3,10 @@
|
|||
"allowJs": true,
|
||||
"strict": true,
|
||||
"outDir": "dist",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"target": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"paths": {
|
||||
"@budibase/*": [
|
||||
"../*/src/index.ts",
|
||||
|
@ -12,6 +16,5 @@
|
|||
],
|
||||
"*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
import { svelte } from "@sveltejs/vite-plugin-svelte"
|
||||
import { defineConfig } from "vite"
|
||||
import path from "path"
|
||||
import cssInjectedByJsPlugin from "vite-plugin-css-injected-by-js"
|
||||
|
||||
const ignoredWarnings = [
|
||||
"unused-export-let",
|
||||
"css-unused-selector",
|
||||
"module-script-reactive-declaration",
|
||||
"a11y-no-onchange",
|
||||
"a11y-click-events-have-key-events",
|
||||
]
|
||||
|
||||
export default defineConfig(({ mode }) => {
|
||||
const isProduction = mode === "production"
|
||||
|
||||
return {
|
||||
server: {
|
||||
open: false,
|
||||
},
|
||||
build: {
|
||||
lib: {
|
||||
entry: "src/index.js",
|
||||
formats: ["iife"],
|
||||
outDir: "dist",
|
||||
name: "budibase_client",
|
||||
fileName: () => "budibase-client.js",
|
||||
minify: isProduction,
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
svelte({
|
||||
emitCss: true,
|
||||
onwarn: (warning, handler) => {
|
||||
// Ignore some warnings
|
||||
if (!ignoredWarnings.includes(warning.code)) {
|
||||
handler(warning)
|
||||
}
|
||||
},
|
||||
}),
|
||||
cssInjectedByJsPlugin(),
|
||||
],
|
||||
resolve: {
|
||||
dedupe: ["svelte", "svelte/internal"],
|
||||
alias: [
|
||||
{
|
||||
find: "manifest.json",
|
||||
replacement: path.resolve("./manifest.json"),
|
||||
},
|
||||
{
|
||||
find: "api",
|
||||
replacement: path.resolve("./src/api"),
|
||||
},
|
||||
{
|
||||
find: "components",
|
||||
replacement: path.resolve("./src/components"),
|
||||
},
|
||||
{
|
||||
find: "stores",
|
||||
replacement: path.resolve("./src/stores"),
|
||||
},
|
||||
{
|
||||
find: "utils",
|
||||
replacement: path.resolve("./src/utils"),
|
||||
},
|
||||
{
|
||||
find: "constants",
|
||||
replacement: path.resolve("./src/constants"),
|
||||
},
|
||||
{
|
||||
find: "sdk",
|
||||
replacement: path.resolve("./src/sdk"),
|
||||
},
|
||||
{
|
||||
find: "@budibase/types",
|
||||
replacement: path.resolve("../types/src"),
|
||||
},
|
||||
{
|
||||
find: "@budibase/shared-core",
|
||||
replacement: path.resolve("../shared-core/src"),
|
||||
},
|
||||
{
|
||||
find: "@budibase/bbui",
|
||||
replacement: path.resolve("../bbui/src"),
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
})
|
|
@ -1 +1 @@
|
|||
Subproject commit 4facf6a44ee52a405794845f71584168b9db652c
|
||||
Subproject commit e8ef2205de8bca5adcf18d07573096086aa9a606
|
|
@ -63,6 +63,7 @@
|
|||
"@bull-board/koa": "5.10.2",
|
||||
"@elastic/elasticsearch": "7.10.0",
|
||||
"@google-cloud/firestore": "7.8.0",
|
||||
"@koa/cors": "5.0.0",
|
||||
"@koa/router": "13.1.0",
|
||||
"@socket.io/redis-adapter": "^8.2.1",
|
||||
"@types/xml2js": "^0.4.14",
|
||||
|
@ -80,8 +81,8 @@
|
|||
"cookies": "0.8.0",
|
||||
"csvtojson": "2.0.10",
|
||||
"curlconverter": "3.21.0",
|
||||
"dd-trace": "5.23.0",
|
||||
"dayjs": "^1.10.8",
|
||||
"dd-trace": "5.26.0",
|
||||
"dotenv": "8.2.0",
|
||||
"form-data": "4.0.0",
|
||||
"global-agent": "3.0.0",
|
||||
|
@ -142,6 +143,7 @@
|
|||
"@types/jest": "29.5.5",
|
||||
"@types/koa": "2.13.4",
|
||||
"@types/koa-send": "^4.1.6",
|
||||
"@types/koa__cors": "5.0.0",
|
||||
"@types/koa__router": "12.0.4",
|
||||
"@types/lodash": "4.14.200",
|
||||
"@types/mssql": "9.1.5",
|
||||
|
@ -174,7 +176,7 @@
|
|||
"tsconfig-paths": "4.0.0",
|
||||
"typescript": "5.5.2",
|
||||
"update-dotenv": "1.1.1",
|
||||
"yargs": "13.2.4",
|
||||
"yargs": "^13.2.4",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"nx": {
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -23,6 +23,13 @@ components:
|
|||
description: The ID of the table which this request is targeting.
|
||||
schema:
|
||||
type: string
|
||||
viewId:
|
||||
in: path
|
||||
name: viewId
|
||||
required: true
|
||||
description: The ID of the view which this request is targeting.
|
||||
schema:
|
||||
type: string
|
||||
rowId:
|
||||
in: path
|
||||
name: rowId
|
||||
|
@ -36,7 +43,7 @@ components:
|
|||
required: true
|
||||
description: The ID of the app which this request is targeting.
|
||||
schema:
|
||||
default: "{{ appId }}"
|
||||
default: "{{appId}}"
|
||||
type: string
|
||||
appIdUrl:
|
||||
in: path
|
||||
|
@ -44,7 +51,7 @@ components:
|
|||
required: true
|
||||
description: The ID of the app which this request is targeting.
|
||||
schema:
|
||||
default: "{{ appId }}"
|
||||
default: "{{appId}}"
|
||||
type: string
|
||||
queryId:
|
||||
in: path
|
||||
|
@ -442,6 +449,74 @@ components:
|
|||
# TYPE budibase_quota_limit_automations gauge
|
||||
|
||||
budibase_quota_limit_automations 9007199254740991
|
||||
view:
|
||||
value:
|
||||
data:
|
||||
name: peopleView
|
||||
tableId: ta_896a325f7e8147d2a2cda93c5d236511
|
||||
schema:
|
||||
name:
|
||||
visible: true
|
||||
readonly: false
|
||||
order: 1
|
||||
width: 300
|
||||
age:
|
||||
visible: true
|
||||
readonly: true
|
||||
order: 2
|
||||
width: 200
|
||||
salary:
|
||||
visible: false
|
||||
readonly: false
|
||||
query:
|
||||
logicalOperator: all
|
||||
onEmptyFilter: none
|
||||
groups:
|
||||
- logicalOperator: any
|
||||
filters:
|
||||
- operator: string
|
||||
field: name
|
||||
value: John
|
||||
- operator: range
|
||||
field: age
|
||||
value:
|
||||
low: 18
|
||||
high: 100
|
||||
primaryDisplay: name
|
||||
views:
|
||||
value:
|
||||
data:
|
||||
- name: peopleView
|
||||
tableId: ta_896a325f7e8147d2a2cda93c5d236511
|
||||
schema:
|
||||
name:
|
||||
visible: true
|
||||
readonly: false
|
||||
order: 1
|
||||
width: 300
|
||||
age:
|
||||
visible: true
|
||||
readonly: true
|
||||
order: 2
|
||||
width: 200
|
||||
salary:
|
||||
visible: false
|
||||
readonly: false
|
||||
query:
|
||||
logicalOperator: all
|
||||
onEmptyFilter: none
|
||||
groups:
|
||||
- logicalOperator: any
|
||||
filters:
|
||||
- operator: string
|
||||
field: name
|
||||
value: John
|
||||
- operator: range
|
||||
field: age
|
||||
value:
|
||||
low: 18
|
||||
high: 100
|
||||
primaryDisplay: name
|
||||
securitySchemes:
|
||||
ApiKeyAuth:
|
||||
type: apiKey
|
||||
|
@ -761,7 +836,6 @@ components:
|
|||
enum:
|
||||
- static
|
||||
- dynamic
|
||||
- ai
|
||||
description: Defines whether this is a static or dynamic formula.
|
||||
- type: object
|
||||
properties:
|
||||
|
@ -931,7 +1005,6 @@ components:
|
|||
enum:
|
||||
- static
|
||||
- dynamic
|
||||
- ai
|
||||
description: Defines whether this is a static or dynamic formula.
|
||||
- type: object
|
||||
properties:
|
||||
|
@ -1108,7 +1181,6 @@ components:
|
|||
enum:
|
||||
- static
|
||||
- dynamic
|
||||
- ai
|
||||
description: Defines whether this is a static or dynamic formula.
|
||||
- type: object
|
||||
properties:
|
||||
|
@ -1704,6 +1776,641 @@ components:
|
|||
- userIds
|
||||
required:
|
||||
- data
|
||||
view:
|
||||
description: The view to be created/updated.
|
||||
type: object
|
||||
required:
|
||||
- name
|
||||
- schema
|
||||
- tableId
|
||||
properties:
|
||||
name:
|
||||
description: The name of the view.
|
||||
type: string
|
||||
tableId:
|
||||
description: The ID of the table this view is based on.
|
||||
type: string
|
||||
type:
|
||||
description: The type of view - standard (empty value) or calculation.
|
||||
type: string
|
||||
enum:
|
||||
- calculation
|
||||
primaryDisplay:
|
||||
type: string
|
||||
description: A column used to display rows from this view - usually used when
|
||||
rendered in tables.
|
||||
query:
|
||||
description: Search parameters for view
|
||||
type: object
|
||||
properties:
|
||||
logicalOperator:
|
||||
description: When using groups this defines whether all of the filters must
|
||||
match, or only one of them.
|
||||
type: string
|
||||
enum:
|
||||
- all
|
||||
- any
|
||||
onEmptyFilter:
|
||||
description: If no filters match, should the view return all rows, or no rows.
|
||||
type: string
|
||||
enum:
|
||||
- all
|
||||
- none
|
||||
groups:
|
||||
description: A grouping of filters to be applied.
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
logicalOperator:
|
||||
description: When using groups this defines whether all of the filters must
|
||||
match, or only one of them.
|
||||
type: string
|
||||
enum:
|
||||
- all
|
||||
- any
|
||||
filters:
|
||||
description: A list of filters to apply
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
operator:
|
||||
type: string
|
||||
description: The type of search operation which is being performed.
|
||||
enum:
|
||||
- equal
|
||||
- notEqual
|
||||
- empty
|
||||
- notEmpty
|
||||
- fuzzy
|
||||
- string
|
||||
- contains
|
||||
- notContains
|
||||
- containsAny
|
||||
- oneOf
|
||||
- range
|
||||
field:
|
||||
type: string
|
||||
description: The field in the view to perform the search on.
|
||||
value:
|
||||
description: The value to search for - the type will depend on the operator in
|
||||
use.
|
||||
oneOf:
|
||||
- type: string
|
||||
- type: number
|
||||
- type: boolean
|
||||
- type: object
|
||||
- type: array
|
||||
groups:
|
||||
description: A grouping of filters to be applied.
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
logicalOperator:
|
||||
description: When using groups this defines whether all of the filters must
|
||||
match, or only one of them.
|
||||
type: string
|
||||
enum:
|
||||
- all
|
||||
- any
|
||||
filters:
|
||||
description: A list of filters to apply
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
operator:
|
||||
type: string
|
||||
description: The type of search operation which is being performed.
|
||||
enum:
|
||||
- equal
|
||||
- notEqual
|
||||
- empty
|
||||
- notEmpty
|
||||
- fuzzy
|
||||
- string
|
||||
- contains
|
||||
- notContains
|
||||
- containsAny
|
||||
- oneOf
|
||||
- range
|
||||
field:
|
||||
type: string
|
||||
description: The field in the view to perform the search on.
|
||||
value:
|
||||
description: The value to search for - the type will depend on the operator in
|
||||
use.
|
||||
oneOf:
|
||||
- type: string
|
||||
- type: number
|
||||
- type: boolean
|
||||
- type: object
|
||||
- type: array
|
||||
sort:
|
||||
type: object
|
||||
required:
|
||||
- field
|
||||
properties:
|
||||
field:
|
||||
type: string
|
||||
description: The field from the table/view schema to sort on.
|
||||
order:
|
||||
type: string
|
||||
description: The order in which to sort.
|
||||
enum:
|
||||
- ascending
|
||||
- descending
|
||||
type:
|
||||
type: string
|
||||
description: The type of sort to perform (by number, or by alphabetically).
|
||||
enum:
|
||||
- string
|
||||
- number
|
||||
schema:
|
||||
type: object
|
||||
additionalProperties:
|
||||
oneOf:
|
||||
- type: object
|
||||
properties:
|
||||
visible:
|
||||
type: boolean
|
||||
description: Defines whether the column is visible or not - rows
|
||||
retrieved/updated through this view will not be able to
|
||||
access it.
|
||||
readonly:
|
||||
type: boolean
|
||||
description: "When used in combination with 'visible: true' the column will be
|
||||
visible in row responses but cannot be updated."
|
||||
order:
|
||||
type: integer
|
||||
description: A number defining where the column shows up in tables, lowest being
|
||||
first.
|
||||
width:
|
||||
type: integer
|
||||
description: A width for the column, defined in pixels - this affects rendering
|
||||
in tables.
|
||||
column:
|
||||
type: array
|
||||
description: If this is a relationship column, we can set the columns we wish to
|
||||
include
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
readonly:
|
||||
type: boolean
|
||||
- type: object
|
||||
properties:
|
||||
calculationType:
|
||||
type: string
|
||||
description: This column should be built from a calculation, specifying a type
|
||||
and field. It is important to note when a calculation is
|
||||
configured all non-calculation columns will be used for
|
||||
grouping.
|
||||
enum:
|
||||
- sum
|
||||
- avg
|
||||
- count
|
||||
- min
|
||||
- max
|
||||
field:
|
||||
type: string
|
||||
description: The field from the table to perform the calculation on.
|
||||
distinct:
|
||||
type: boolean
|
||||
description: Can be used in tandem with the count calculation type, to count
|
||||
unique entries.
|
||||
viewOutput:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
description: The view to be created/updated.
|
||||
type: object
|
||||
required:
|
||||
- name
|
||||
- schema
|
||||
- tableId
|
||||
- id
|
||||
properties:
|
||||
name:
|
||||
description: The name of the view.
|
||||
type: string
|
||||
tableId:
|
||||
description: The ID of the table this view is based on.
|
||||
type: string
|
||||
type:
|
||||
description: The type of view - standard (empty value) or calculation.
|
||||
type: string
|
||||
enum:
|
||||
- calculation
|
||||
primaryDisplay:
|
||||
type: string
|
||||
description: A column used to display rows from this view - usually used when
|
||||
rendered in tables.
|
||||
query:
|
||||
description: Search parameters for view
|
||||
type: object
|
||||
properties:
|
||||
logicalOperator:
|
||||
description: When using groups this defines whether all of the filters must
|
||||
match, or only one of them.
|
||||
type: string
|
||||
enum:
|
||||
- all
|
||||
- any
|
||||
onEmptyFilter:
|
||||
description: If no filters match, should the view return all rows, or no rows.
|
||||
type: string
|
||||
enum:
|
||||
- all
|
||||
- none
|
||||
groups:
|
||||
description: A grouping of filters to be applied.
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
logicalOperator:
|
||||
description: When using groups this defines whether all of the filters must
|
||||
match, or only one of them.
|
||||
type: string
|
||||
enum:
|
||||
- all
|
||||
- any
|
||||
filters:
|
||||
description: A list of filters to apply
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
operator:
|
||||
type: string
|
||||
description: The type of search operation which is being performed.
|
||||
enum:
|
||||
- equal
|
||||
- notEqual
|
||||
- empty
|
||||
- notEmpty
|
||||
- fuzzy
|
||||
- string
|
||||
- contains
|
||||
- notContains
|
||||
- containsAny
|
||||
- oneOf
|
||||
- range
|
||||
field:
|
||||
type: string
|
||||
description: The field in the view to perform the search on.
|
||||
value:
|
||||
description: The value to search for - the type will depend on the operator in
|
||||
use.
|
||||
oneOf:
|
||||
- type: string
|
||||
- type: number
|
||||
- type: boolean
|
||||
- type: object
|
||||
- type: array
|
||||
groups:
|
||||
description: A grouping of filters to be applied.
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
logicalOperator:
|
||||
description: When using groups this defines whether all of the filters must
|
||||
match, or only one of them.
|
||||
type: string
|
||||
enum:
|
||||
- all
|
||||
- any
|
||||
filters:
|
||||
description: A list of filters to apply
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
operator:
|
||||
type: string
|
||||
description: The type of search operation which is being performed.
|
||||
enum:
|
||||
- equal
|
||||
- notEqual
|
||||
- empty
|
||||
- notEmpty
|
||||
- fuzzy
|
||||
- string
|
||||
- contains
|
||||
- notContains
|
||||
- containsAny
|
||||
- oneOf
|
||||
- range
|
||||
field:
|
||||
type: string
|
||||
description: The field in the view to perform the search on.
|
||||
value:
|
||||
description: The value to search for - the type will depend on the operator in
|
||||
use.
|
||||
oneOf:
|
||||
- type: string
|
||||
- type: number
|
||||
- type: boolean
|
||||
- type: object
|
||||
- type: array
|
||||
sort:
|
||||
type: object
|
||||
required:
|
||||
- field
|
||||
properties:
|
||||
field:
|
||||
type: string
|
||||
description: The field from the table/view schema to sort on.
|
||||
order:
|
||||
type: string
|
||||
description: The order in which to sort.
|
||||
enum:
|
||||
- ascending
|
||||
- descending
|
||||
type:
|
||||
type: string
|
||||
description: The type of sort to perform (by number, or by alphabetically).
|
||||
enum:
|
||||
- string
|
||||
- number
|
||||
schema:
|
||||
type: object
|
||||
additionalProperties:
|
||||
oneOf:
|
||||
- type: object
|
||||
properties:
|
||||
visible:
|
||||
type: boolean
|
||||
description: Defines whether the column is visible or not - rows
|
||||
retrieved/updated through this view will not be able
|
||||
to access it.
|
||||
readonly:
|
||||
type: boolean
|
||||
description: "When used in combination with 'visible: true' the column will be
|
||||
visible in row responses but cannot be updated."
|
||||
order:
|
||||
type: integer
|
||||
description: A number defining where the column shows up in tables, lowest being
|
||||
first.
|
||||
width:
|
||||
type: integer
|
||||
description: A width for the column, defined in pixels - this affects rendering
|
||||
in tables.
|
||||
column:
|
||||
type: array
|
||||
description: If this is a relationship column, we can set the columns we wish to
|
||||
include
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
readonly:
|
||||
type: boolean
|
||||
- type: object
|
||||
properties:
|
||||
calculationType:
|
||||
type: string
|
||||
description: This column should be built from a calculation, specifying a type
|
||||
and field. It is important to note when a calculation
|
||||
is configured all non-calculation columns will be used
|
||||
for grouping.
|
||||
enum:
|
||||
- sum
|
||||
- avg
|
||||
- count
|
||||
- min
|
||||
- max
|
||||
field:
|
||||
type: string
|
||||
description: The field from the table to perform the calculation on.
|
||||
distinct:
|
||||
type: boolean
|
||||
description: Can be used in tandem with the count calculation type, to count
|
||||
unique entries.
|
||||
id:
|
||||
description: The ID of the view.
|
||||
type: string
|
||||
required:
|
||||
- data
|
||||
viewSearch:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
description: The view to be created/updated.
|
||||
type: object
|
||||
required:
|
||||
- name
|
||||
- schema
|
||||
- tableId
|
||||
- id
|
||||
properties:
|
||||
name:
|
||||
description: The name of the view.
|
||||
type: string
|
||||
tableId:
|
||||
description: The ID of the table this view is based on.
|
||||
type: string
|
||||
type:
|
||||
description: The type of view - standard (empty value) or calculation.
|
||||
type: string
|
||||
enum:
|
||||
- calculation
|
||||
primaryDisplay:
|
||||
type: string
|
||||
description: A column used to display rows from this view - usually used when
|
||||
rendered in tables.
|
||||
query:
|
||||
description: Search parameters for view
|
||||
type: object
|
||||
properties:
|
||||
logicalOperator:
|
||||
description: When using groups this defines whether all of the filters must
|
||||
match, or only one of them.
|
||||
type: string
|
||||
enum:
|
||||
- all
|
||||
- any
|
||||
onEmptyFilter:
|
||||
description: If no filters match, should the view return all rows, or no rows.
|
||||
type: string
|
||||
enum:
|
||||
- all
|
||||
- none
|
||||
groups:
|
||||
description: A grouping of filters to be applied.
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
logicalOperator:
|
||||
description: When using groups this defines whether all of the filters must
|
||||
match, or only one of them.
|
||||
type: string
|
||||
enum:
|
||||
- all
|
||||
- any
|
||||
filters:
|
||||
description: A list of filters to apply
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
operator:
|
||||
type: string
|
||||
description: The type of search operation which is being performed.
|
||||
enum:
|
||||
- equal
|
||||
- notEqual
|
||||
- empty
|
||||
- notEmpty
|
||||
- fuzzy
|
||||
- string
|
||||
- contains
|
||||
- notContains
|
||||
- containsAny
|
||||
- oneOf
|
||||
- range
|
||||
field:
|
||||
type: string
|
||||
description: The field in the view to perform the search on.
|
||||
value:
|
||||
description: The value to search for - the type will depend on the operator in
|
||||
use.
|
||||
oneOf:
|
||||
- type: string
|
||||
- type: number
|
||||
- type: boolean
|
||||
- type: object
|
||||
- type: array
|
||||
groups:
|
||||
description: A grouping of filters to be applied.
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
logicalOperator:
|
||||
description: When using groups this defines whether all of the filters must
|
||||
match, or only one of them.
|
||||
type: string
|
||||
enum:
|
||||
- all
|
||||
- any
|
||||
filters:
|
||||
description: A list of filters to apply
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
operator:
|
||||
type: string
|
||||
description: The type of search operation which is being performed.
|
||||
enum:
|
||||
- equal
|
||||
- notEqual
|
||||
- empty
|
||||
- notEmpty
|
||||
- fuzzy
|
||||
- string
|
||||
- contains
|
||||
- notContains
|
||||
- containsAny
|
||||
- oneOf
|
||||
- range
|
||||
field:
|
||||
type: string
|
||||
description: The field in the view to perform the search on.
|
||||
value:
|
||||
description: The value to search for - the type will depend on the operator in
|
||||
use.
|
||||
oneOf:
|
||||
- type: string
|
||||
- type: number
|
||||
- type: boolean
|
||||
- type: object
|
||||
- type: array
|
||||
sort:
|
||||
type: object
|
||||
required:
|
||||
- field
|
||||
properties:
|
||||
field:
|
||||
type: string
|
||||
description: The field from the table/view schema to sort on.
|
||||
order:
|
||||
type: string
|
||||
description: The order in which to sort.
|
||||
enum:
|
||||
- ascending
|
||||
- descending
|
||||
type:
|
||||
type: string
|
||||
description: The type of sort to perform (by number, or by alphabetically).
|
||||
enum:
|
||||
- string
|
||||
- number
|
||||
schema:
|
||||
type: object
|
||||
additionalProperties:
|
||||
oneOf:
|
||||
- type: object
|
||||
properties:
|
||||
visible:
|
||||
type: boolean
|
||||
description: Defines whether the column is visible or not - rows
|
||||
retrieved/updated through this view will not be able
|
||||
to access it.
|
||||
readonly:
|
||||
type: boolean
|
||||
description: "When used in combination with 'visible: true' the column will be
|
||||
visible in row responses but cannot be updated."
|
||||
order:
|
||||
type: integer
|
||||
description: A number defining where the column shows up in tables, lowest being
|
||||
first.
|
||||
width:
|
||||
type: integer
|
||||
description: A width for the column, defined in pixels - this affects rendering
|
||||
in tables.
|
||||
column:
|
||||
type: array
|
||||
description: If this is a relationship column, we can set the columns we wish to
|
||||
include
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
readonly:
|
||||
type: boolean
|
||||
- type: object
|
||||
properties:
|
||||
calculationType:
|
||||
type: string
|
||||
description: This column should be built from a calculation, specifying a type
|
||||
and field. It is important to note when a
|
||||
calculation is configured all non-calculation
|
||||
columns will be used for grouping.
|
||||
enum:
|
||||
- sum
|
||||
- avg
|
||||
- count
|
||||
- min
|
||||
- max
|
||||
field:
|
||||
type: string
|
||||
description: The field from the table to perform the calculation on.
|
||||
distinct:
|
||||
type: boolean
|
||||
description: Can be used in tandem with the count calculation type, to count
|
||||
unique entries.
|
||||
id:
|
||||
description: The ID of the view.
|
||||
type: string
|
||||
required:
|
||||
- data
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
paths:
|
||||
|
@ -2136,6 +2843,32 @@ paths:
|
|||
examples:
|
||||
search:
|
||||
$ref: "#/components/examples/rows"
|
||||
"/views/{viewId}/rows/search":
|
||||
post:
|
||||
operationId: rowViewSearch
|
||||
summary: Search for rows in a view
|
||||
tags:
|
||||
- rows
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/viewId"
|
||||
- $ref: "#/components/parameters/appId"
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/rowSearch"
|
||||
responses:
|
||||
"200":
|
||||
description: The response will contain an array of rows that match the search
|
||||
parameters.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/searchOutput"
|
||||
examples:
|
||||
search:
|
||||
$ref: "#/components/examples/rows"
|
||||
/tables:
|
||||
post:
|
||||
operationId: tableCreate
|
||||
|
@ -2359,4 +3092,123 @@ paths:
|
|||
examples:
|
||||
users:
|
||||
$ref: "#/components/examples/users"
|
||||
/views:
|
||||
post:
|
||||
operationId: viewCreate
|
||||
summary: Create a view
|
||||
description: Create a view, this can be against an internal or external table.
|
||||
tags:
|
||||
- views
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/appId"
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/view"
|
||||
examples:
|
||||
view:
|
||||
$ref: "#/components/examples/view"
|
||||
responses:
|
||||
"200":
|
||||
description: Returns the created view, including the ID which has been generated
|
||||
for it.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/viewOutput"
|
||||
examples:
|
||||
view:
|
||||
$ref: "#/components/examples/view"
|
||||
"/views/{viewId}":
|
||||
put:
|
||||
operationId: viewUpdate
|
||||
summary: Update a view
|
||||
description: Update a view, this can be against an internal or external table.
|
||||
tags:
|
||||
- views
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/viewId"
|
||||
- $ref: "#/components/parameters/appId"
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/view"
|
||||
examples:
|
||||
view:
|
||||
$ref: "#/components/examples/view"
|
||||
responses:
|
||||
"200":
|
||||
description: Returns the updated view.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/viewOutput"
|
||||
examples:
|
||||
view:
|
||||
$ref: "#/components/examples/view"
|
||||
delete:
|
||||
operationId: viewDestroy
|
||||
summary: Delete a view
|
||||
description: Delete a view, this can be against an internal or external table.
|
||||
tags:
|
||||
- views
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/viewId"
|
||||
- $ref: "#/components/parameters/appId"
|
||||
responses:
|
||||
"200":
|
||||
description: Returns the deleted view.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/viewOutput"
|
||||
examples:
|
||||
view:
|
||||
$ref: "#/components/examples/view"
|
||||
get:
|
||||
operationId: viewGetById
|
||||
summary: Retrieve a view
|
||||
description: Lookup a view, this could be internal or external.
|
||||
tags:
|
||||
- views
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/viewId"
|
||||
- $ref: "#/components/parameters/appId"
|
||||
responses:
|
||||
"200":
|
||||
description: Returns the retrieved view.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/viewOutput"
|
||||
examples:
|
||||
view:
|
||||
$ref: "#/components/examples/view"
|
||||
/views/search:
|
||||
post:
|
||||
operationId: viewSearch
|
||||
summary: Search for views
|
||||
description: Based on view properties (currently only name) search for views.
|
||||
tags:
|
||||
- views
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/appId"
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/nameSearch"
|
||||
responses:
|
||||
"200":
|
||||
description: Returns the found views, based on the search parameters.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/viewSearch"
|
||||
examples:
|
||||
views:
|
||||
$ref: "#/components/examples/views"
|
||||
tags: []
|
||||
|
|
|
@ -8,6 +8,16 @@ export const tableId = {
|
|||
},
|
||||
}
|
||||
|
||||
export const viewId = {
|
||||
in: "path",
|
||||
name: "viewId",
|
||||
required: true,
|
||||
description: "The ID of the view which this request is targeting.",
|
||||
schema: {
|
||||
type: "string",
|
||||
},
|
||||
}
|
||||
|
||||
export const rowId = {
|
||||
in: "path",
|
||||
name: "rowId",
|
||||
|
|
|
@ -6,6 +6,7 @@ import user from "./user"
|
|||
import metrics from "./metrics"
|
||||
import misc from "./misc"
|
||||
import roles from "./roles"
|
||||
import view from "./view"
|
||||
|
||||
export const examples = {
|
||||
...application.getExamples(),
|
||||
|
@ -16,6 +17,7 @@ export const examples = {
|
|||
...misc.getExamples(),
|
||||
...metrics.getExamples(),
|
||||
...roles.getExamples(),
|
||||
...view.getExamples(),
|
||||
}
|
||||
|
||||
export const schemas = {
|
||||
|
@ -26,4 +28,5 @@ export const schemas = {
|
|||
...user.getSchemas(),
|
||||
...misc.getSchemas(),
|
||||
...roles.getSchemas(),
|
||||
...view.getSchemas(),
|
||||
}
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
import { object } from "./utils"
|
||||
import Resource from "./utils/Resource"
|
||||
|
||||
export default new Resource().setSchemas({
|
||||
rowSearch: object(
|
||||
{
|
||||
query: {
|
||||
export const searchSchema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
allOr: {
|
||||
|
@ -93,7 +90,12 @@ export default new Resource().setSchemas({
|
|||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default new Resource().setSchemas({
|
||||
rowSearch: object(
|
||||
{
|
||||
query: searchSchema,
|
||||
paginate: {
|
||||
type: "boolean",
|
||||
description: "Enables pagination, by default this is disabled.",
|
||||
|
|
|
@ -0,0 +1,273 @@
|
|||
import { object } from "./utils"
|
||||
import Resource from "./utils/Resource"
|
||||
import {
|
||||
ArrayOperator,
|
||||
BasicOperator,
|
||||
CalculationType,
|
||||
RangeOperator,
|
||||
SortOrder,
|
||||
SortType,
|
||||
} from "@budibase/types"
|
||||
import { cloneDeep } from "lodash"
|
||||
|
||||
const view = {
|
||||
name: "peopleView",
|
||||
tableId: "ta_896a325f7e8147d2a2cda93c5d236511",
|
||||
schema: {
|
||||
name: {
|
||||
visible: true,
|
||||
readonly: false,
|
||||
order: 1,
|
||||
width: 300,
|
||||
},
|
||||
age: {
|
||||
visible: true,
|
||||
readonly: true,
|
||||
order: 2,
|
||||
width: 200,
|
||||
},
|
||||
salary: {
|
||||
visible: false,
|
||||
readonly: false,
|
||||
},
|
||||
},
|
||||
query: {
|
||||
logicalOperator: "all",
|
||||
onEmptyFilter: "none",
|
||||
groups: [
|
||||
{
|
||||
logicalOperator: "any",
|
||||
filters: [
|
||||
{ operator: "string", field: "name", value: "John" },
|
||||
{ operator: "range", field: "age", value: { low: 18, high: 100 } },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
primaryDisplay: "name",
|
||||
}
|
||||
|
||||
const baseColumnDef = {
|
||||
visible: {
|
||||
type: "boolean",
|
||||
description:
|
||||
"Defines whether the column is visible or not - rows retrieved/updated through this view will not be able to access it.",
|
||||
},
|
||||
readonly: {
|
||||
type: "boolean",
|
||||
description:
|
||||
"When used in combination with 'visible: true' the column will be visible in row responses but cannot be updated.",
|
||||
},
|
||||
order: {
|
||||
type: "integer",
|
||||
description:
|
||||
"A number defining where the column shows up in tables, lowest being first.",
|
||||
},
|
||||
width: {
|
||||
type: "integer",
|
||||
description:
|
||||
"A width for the column, defined in pixels - this affects rendering in tables.",
|
||||
},
|
||||
column: {
|
||||
type: "array",
|
||||
description:
|
||||
"If this is a relationship column, we can set the columns we wish to include",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
readonly: {
|
||||
type: "boolean",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const logicalOperator = {
|
||||
description:
|
||||
"When using groups this defines whether all of the filters must match, or only one of them.",
|
||||
type: "string",
|
||||
enum: ["all", "any"],
|
||||
}
|
||||
|
||||
const filterGroup = {
|
||||
description: "A grouping of filters to be applied.",
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
logicalOperator,
|
||||
filters: {
|
||||
description: "A list of filters to apply",
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
operator: {
|
||||
type: "string",
|
||||
description:
|
||||
"The type of search operation which is being performed.",
|
||||
enum: [
|
||||
...Object.values(BasicOperator),
|
||||
...Object.values(ArrayOperator),
|
||||
...Object.values(RangeOperator),
|
||||
],
|
||||
},
|
||||
field: {
|
||||
type: "string",
|
||||
description: "The field in the view to perform the search on.",
|
||||
},
|
||||
value: {
|
||||
description:
|
||||
"The value to search for - the type will depend on the operator in use.",
|
||||
oneOf: [
|
||||
{ type: "string" },
|
||||
{ type: "number" },
|
||||
{ type: "boolean" },
|
||||
{ type: "object" },
|
||||
{ type: "array" },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// have to clone to avoid constantly recursive structure - we can't represent this easily
|
||||
const layeredFilterGroup: any = cloneDeep(filterGroup)
|
||||
layeredFilterGroup.items.properties.groups = filterGroup
|
||||
|
||||
const viewQuerySchema = {
|
||||
description: "Search parameters for view",
|
||||
type: "object",
|
||||
properties: {
|
||||
logicalOperator,
|
||||
onEmptyFilter: {
|
||||
description:
|
||||
"If no filters match, should the view return all rows, or no rows.",
|
||||
type: "string",
|
||||
enum: ["all", "none"],
|
||||
},
|
||||
groups: layeredFilterGroup,
|
||||
},
|
||||
}
|
||||
|
||||
const viewSchema = {
|
||||
description: "The view to be created/updated.",
|
||||
type: "object",
|
||||
required: ["name", "schema", "tableId"],
|
||||
properties: {
|
||||
name: {
|
||||
description: "The name of the view.",
|
||||
type: "string",
|
||||
},
|
||||
tableId: {
|
||||
description: "The ID of the table this view is based on.",
|
||||
type: "string",
|
||||
},
|
||||
type: {
|
||||
description: "The type of view - standard (empty value) or calculation.",
|
||||
type: "string",
|
||||
enum: ["calculation"],
|
||||
},
|
||||
primaryDisplay: {
|
||||
type: "string",
|
||||
description:
|
||||
"A column used to display rows from this view - usually used when rendered in tables.",
|
||||
},
|
||||
query: viewQuerySchema,
|
||||
sort: {
|
||||
type: "object",
|
||||
required: ["field"],
|
||||
properties: {
|
||||
field: {
|
||||
type: "string",
|
||||
description: "The field from the table/view schema to sort on.",
|
||||
},
|
||||
order: {
|
||||
type: "string",
|
||||
description: "The order in which to sort.",
|
||||
enum: Object.values(SortOrder),
|
||||
},
|
||||
type: {
|
||||
type: "string",
|
||||
description:
|
||||
"The type of sort to perform (by number, or by alphabetically).",
|
||||
enum: Object.values(SortType),
|
||||
},
|
||||
},
|
||||
},
|
||||
schema: {
|
||||
type: "object",
|
||||
additionalProperties: {
|
||||
oneOf: [
|
||||
{
|
||||
type: "object",
|
||||
properties: baseColumnDef,
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
calculationType: {
|
||||
type: "string",
|
||||
description:
|
||||
"This column should be built from a calculation, specifying a type and field. It is important to note when a calculation is configured all non-calculation columns will be used for grouping.",
|
||||
enum: Object.values(CalculationType),
|
||||
},
|
||||
field: {
|
||||
type: "string",
|
||||
description:
|
||||
"The field from the table to perform the calculation on.",
|
||||
},
|
||||
distinct: {
|
||||
type: "boolean",
|
||||
description:
|
||||
"Can be used in tandem with the count calculation type, to count unique entries.",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const viewOutputSchema = {
|
||||
...viewSchema,
|
||||
properties: {
|
||||
...viewSchema.properties,
|
||||
id: {
|
||||
description: "The ID of the view.",
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
required: [...viewSchema.required, "id"],
|
||||
}
|
||||
|
||||
export default new Resource()
|
||||
.setExamples({
|
||||
view: {
|
||||
value: {
|
||||
data: view,
|
||||
},
|
||||
},
|
||||
views: {
|
||||
value: {
|
||||
data: [view],
|
||||
},
|
||||
},
|
||||
})
|
||||
.setSchemas({
|
||||
view: viewSchema,
|
||||
viewOutput: object({
|
||||
data: viewOutputSchema,
|
||||
}),
|
||||
viewSearch: object({
|
||||
data: {
|
||||
type: "array",
|
||||
items: viewOutputSchema,
|
||||
},
|
||||
}),
|
||||
})
|
|
@ -1,6 +1,7 @@
|
|||
import { Application } from "./types"
|
||||
import { RequiredKeys } from "@budibase/types"
|
||||
|
||||
function application(body: any): Application {
|
||||
function application(body: any): RequiredKeys<Application> {
|
||||
let app = body?.application ? body.application : body
|
||||
return {
|
||||
_id: app.appId,
|
||||
|
|
|
@ -3,6 +3,7 @@ import applications from "./applications"
|
|||
import users from "./users"
|
||||
import rows from "./rows"
|
||||
import queries from "./queries"
|
||||
import views from "./views"
|
||||
|
||||
export default {
|
||||
...tables,
|
||||
|
@ -10,4 +11,5 @@ export default {
|
|||
...users,
|
||||
...rows,
|
||||
...queries,
|
||||
...views,
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { Query, ExecuteQuery } from "./types"
|
||||
import { RequiredKeys } from "@budibase/types"
|
||||
|
||||
function query(body: any): Query {
|
||||
function query(body: any): RequiredKeys<Query> {
|
||||
return {
|
||||
_id: body._id,
|
||||
datasourceId: body.datasourceId,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { Row, RowSearch } from "./types"
|
||||
import { RequiredKeys } from "@budibase/types"
|
||||
|
||||
function row(body: any): Row {
|
||||
function row(body: any): RequiredKeys<Row> {
|
||||
delete body._rev
|
||||
// have to input everything, since structure unknown
|
||||
return {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { Table } from "./types"
|
||||
import { RequiredKeys } from "@budibase/types"
|
||||
|
||||
function table(body: any): Table {
|
||||
function table(body: any): RequiredKeys<Table> {
|
||||
return {
|
||||
_id: body._id,
|
||||
name: body.name,
|
||||
|
|
|
@ -9,6 +9,9 @@ export type CreateApplicationParams = components["schemas"]["application"]
|
|||
export type Table = components["schemas"]["tableOutput"]["data"]
|
||||
export type CreateTableParams = components["schemas"]["table"]
|
||||
|
||||
export type View = components["schemas"]["viewOutput"]["data"]
|
||||
export type CreateViewParams = components["schemas"]["view"]
|
||||
|
||||
export type Row = components["schemas"]["rowOutput"]["data"]
|
||||
export type RowSearch = components["schemas"]["searchOutput"]
|
||||
export type CreateRowParams = components["schemas"]["row"]
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { User } from "./types"
|
||||
import { RequiredKeys } from "@budibase/types"
|
||||
|
||||
function user(body: any): User {
|
||||
function user(body: any): RequiredKeys<User> {
|
||||
return {
|
||||
_id: body._id,
|
||||
email: body.email,
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
import { View } from "./types"
|
||||
import { ViewV2, Ctx, RequiredKeys } from "@budibase/types"
|
||||
import { dataFilters } from "@budibase/shared-core"
|
||||
|
||||
function view(body: ViewV2): RequiredKeys<View> {
|
||||
return {
|
||||
id: body.id,
|
||||
tableId: body.tableId,
|
||||
type: body.type,
|
||||
name: body.name,
|
||||
schema: body.schema!,
|
||||
primaryDisplay: body.primaryDisplay,
|
||||
query: dataFilters.buildQuery(body.query),
|
||||
sort: body.sort,
|
||||
}
|
||||
}
|
||||
|
||||
function mapView(ctx: Ctx<{ data: ViewV2 }>): { data: View } {
|
||||
return {
|
||||
data: view(ctx.body.data),
|
||||
}
|
||||
}
|
||||
|
||||
function mapViews(ctx: Ctx<{ data: ViewV2[] }>): { data: View[] } {
|
||||
const views = ctx.body.data.map((body: ViewV2) => view(body))
|
||||
return { data: views }
|
||||
}
|
||||
|
||||
export default {
|
||||
mapView,
|
||||
mapViews,
|
||||
}
|
|
@ -22,13 +22,13 @@ export function fixRow(row: Row, params: any) {
|
|||
return row
|
||||
}
|
||||
|
||||
export async function search(ctx: UserCtx, next: Next) {
|
||||
function buildSearchRequestBody(ctx: UserCtx) {
|
||||
let { sort, paginate, bookmark, limit, query } = ctx.request.body
|
||||
// update the body to the correct format of the internal search
|
||||
if (!sort) {
|
||||
sort = {}
|
||||
}
|
||||
ctx.request.body = {
|
||||
return {
|
||||
sort: sort.column,
|
||||
sortType: sort.type,
|
||||
sortOrder: sort.order,
|
||||
|
@ -37,10 +37,23 @@ export async function search(ctx: UserCtx, next: Next) {
|
|||
limit,
|
||||
query,
|
||||
}
|
||||
}
|
||||
|
||||
export async function search(ctx: UserCtx, next: Next) {
|
||||
ctx.request.body = buildSearchRequestBody(ctx)
|
||||
await rowController.search(ctx)
|
||||
await next()
|
||||
}
|
||||
|
||||
export async function viewSearch(ctx: UserCtx, next: Next) {
|
||||
ctx.request.body = buildSearchRequestBody(ctx)
|
||||
ctx.params = {
|
||||
viewId: ctx.params.viewId,
|
||||
}
|
||||
await rowController.views.searchView(ctx)
|
||||
await next()
|
||||
}
|
||||
|
||||
export async function create(ctx: UserCtx, next: Next) {
|
||||
ctx.request.body = fixRow(ctx.request.body, ctx.params)
|
||||
await rowController.save(ctx)
|
||||
|
@ -79,4 +92,5 @@ export default {
|
|||
update,
|
||||
destroy,
|
||||
search,
|
||||
viewSearch,
|
||||
}
|
||||
|
|
|
@ -0,0 +1,95 @@
|
|||
import { search as stringSearch } from "./utils"
|
||||
import * as controller from "../view"
|
||||
import { ViewV2, UserCtx, UISearchFilter, PublicAPIView } from "@budibase/types"
|
||||
import { Next } from "koa"
|
||||
import { merge } from "lodash"
|
||||
|
||||
function viewRequest(view: PublicAPIView, params?: { viewId: string }) {
|
||||
const viewV2: ViewV2 = view
|
||||
if (!viewV2) {
|
||||
return viewV2
|
||||
}
|
||||
if (params?.viewId) {
|
||||
viewV2.id = params.viewId
|
||||
}
|
||||
if (!view.query) {
|
||||
viewV2.query = {}
|
||||
} else {
|
||||
// public API only has one form of query
|
||||
viewV2.queryUI = viewV2.query as UISearchFilter
|
||||
}
|
||||
viewV2.version = 2
|
||||
return viewV2
|
||||
}
|
||||
|
||||
function viewResponse(view: ViewV2): PublicAPIView {
|
||||
// remove our internal structure - always un-necessary
|
||||
delete view.query
|
||||
return {
|
||||
...view,
|
||||
query: view.queryUI,
|
||||
}
|
||||
}
|
||||
|
||||
function viewsResponse(views: ViewV2[]): PublicAPIView[] {
|
||||
return views.map(viewResponse)
|
||||
}
|
||||
|
||||
export async function search(ctx: UserCtx, next: Next) {
|
||||
const { name } = ctx.request.body
|
||||
await controller.v2.fetch(ctx)
|
||||
ctx.body.data = viewsResponse(stringSearch(ctx.body.data, name))
|
||||
await next()
|
||||
}
|
||||
|
||||
export async function create(ctx: UserCtx, next: Next) {
|
||||
ctx = merge(ctx, {
|
||||
request: {
|
||||
body: viewRequest(ctx.request.body),
|
||||
},
|
||||
})
|
||||
await controller.v2.create(ctx)
|
||||
ctx.body.data = viewResponse(ctx.body.data)
|
||||
await next()
|
||||
}
|
||||
|
||||
export async function read(ctx: UserCtx, next: Next) {
|
||||
ctx = merge(ctx, {
|
||||
params: {
|
||||
viewId: ctx.params.viewId,
|
||||
},
|
||||
})
|
||||
await controller.v2.get(ctx)
|
||||
ctx.body.data = viewResponse(ctx.body.data)
|
||||
await next()
|
||||
}
|
||||
|
||||
export async function update(ctx: UserCtx, next: Next) {
|
||||
const viewId = ctx.params.viewId
|
||||
ctx = merge(ctx, {
|
||||
request: {
|
||||
body: {
|
||||
data: viewRequest(ctx.request.body, { viewId }),
|
||||
},
|
||||
},
|
||||
params: {
|
||||
viewId,
|
||||
},
|
||||
})
|
||||
await controller.v2.update(ctx)
|
||||
ctx.body.data = viewResponse(ctx.body.data)
|
||||
await next()
|
||||
}
|
||||
|
||||
export async function destroy(ctx: UserCtx, next: Next) {
|
||||
await controller.v2.remove(ctx)
|
||||
await next()
|
||||
}
|
||||
|
||||
export default {
|
||||
create,
|
||||
read,
|
||||
update,
|
||||
destroy,
|
||||
search,
|
||||
}
|
|
@ -51,6 +51,7 @@ export async function searchView(
|
|||
result.rows.forEach(r => (r._viewId = view.id))
|
||||
ctx.body = result
|
||||
}
|
||||
|
||||
function getSortOptions(request: SearchViewRowRequest, view: ViewV2) {
|
||||
if (request.sort) {
|
||||
return {
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
RelationSchemaField,
|
||||
ViewFieldMetadata,
|
||||
CalculationType,
|
||||
ViewFetchResponseEnriched,
|
||||
CountDistinctCalculationFieldMetadata,
|
||||
CountCalculationFieldMetadata,
|
||||
} from "@budibase/types"
|
||||
|
@ -125,6 +126,12 @@ export async function get(ctx: Ctx<void, ViewResponseEnriched>) {
|
|||
}
|
||||
}
|
||||
|
||||
export async function fetch(ctx: Ctx<void, ViewFetchResponseEnriched>) {
|
||||
ctx.body = {
|
||||
data: await sdk.views.getAllEnriched(),
|
||||
}
|
||||
}
|
||||
|
||||
export async function create(ctx: Ctx<CreateViewRequest, ViewResponse>) {
|
||||
const view = ctx.request.body
|
||||
const { tableId } = view
|
||||
|
|
|
@ -4,19 +4,21 @@ import queryEndpoints from "./queries"
|
|||
import tableEndpoints from "./tables"
|
||||
import rowEndpoints from "./rows"
|
||||
import userEndpoints from "./users"
|
||||
import viewEndpoints from "./views"
|
||||
import roleEndpoints from "./roles"
|
||||
import authorized from "../../../middleware/authorized"
|
||||
import publicApi from "../../../middleware/publicApi"
|
||||
import { paramResource, paramSubResource } from "../../../middleware/resourceId"
|
||||
import { PermissionType, PermissionLevel } from "@budibase/types"
|
||||
import { PermissionLevel, PermissionType } from "@budibase/types"
|
||||
import { CtxFn } from "./utils/Endpoint"
|
||||
import mapperMiddleware from "./middleware/mapper"
|
||||
import env from "../../../environment"
|
||||
import { middleware, redis } from "@budibase/backend-core"
|
||||
import { SelectableDatabase } from "@budibase/backend-core/src/redis/utils"
|
||||
import cors from "@koa/cors"
|
||||
// below imports don't have declaration files
|
||||
const Router = require("@koa/router")
|
||||
const { RateLimit, Stores } = require("koa2-ratelimit")
|
||||
import { middleware, redis } from "@budibase/backend-core"
|
||||
import { SelectableDatabase } from "@budibase/backend-core/src/redis/utils"
|
||||
|
||||
interface KoaRateLimitOptions {
|
||||
socket: {
|
||||
|
@ -81,6 +83,7 @@ const publicRouter = new Router({
|
|||
if (limiter && !env.isDev()) {
|
||||
publicRouter.use(limiter)
|
||||
}
|
||||
publicRouter.use(cors())
|
||||
|
||||
function addMiddleware(
|
||||
endpoints: any,
|
||||
|
@ -149,6 +152,7 @@ applyAdminRoutes(metricEndpoints)
|
|||
applyAdminRoutes(roleEndpoints)
|
||||
applyRoutes(appEndpoints, PermissionType.APP, "appId")
|
||||
applyRoutes(tableEndpoints, PermissionType.TABLE, "tableId")
|
||||
applyRoutes(viewEndpoints, PermissionType.VIEW, "viewId")
|
||||
applyRoutes(userEndpoints, PermissionType.USER, "userId")
|
||||
applyRoutes(queryEndpoints, PermissionType.QUERY, "queryId")
|
||||
// needs to be applied last for routing purposes, don't override other endpoints
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import { Ctx } from "@budibase/types"
|
||||
import mapping from "../../../controllers/public/mapping"
|
||||
|
||||
enum Resources {
|
||||
enum Resource {
|
||||
APPLICATION = "applications",
|
||||
TABLES = "tables",
|
||||
VIEWS = "views",
|
||||
ROWS = "rows",
|
||||
USERS = "users",
|
||||
QUERIES = "queries",
|
||||
|
@ -15,7 +16,7 @@ function isAttachment(ctx: Ctx) {
|
|||
}
|
||||
|
||||
function isArrayResponse(ctx: Ctx) {
|
||||
return ctx.url.endsWith(Resources.SEARCH) || Array.isArray(ctx.body)
|
||||
return ctx.url.endsWith(Resource.SEARCH) || Array.isArray(ctx.body)
|
||||
}
|
||||
|
||||
function noResponse(ctx: Ctx) {
|
||||
|
@ -38,6 +39,14 @@ function processTables(ctx: Ctx) {
|
|||
}
|
||||
}
|
||||
|
||||
function processViews(ctx: Ctx) {
|
||||
if (isArrayResponse(ctx)) {
|
||||
return mapping.mapViews(ctx)
|
||||
} else {
|
||||
return mapping.mapView(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
function processRows(ctx: Ctx) {
|
||||
if (isArrayResponse(ctx)) {
|
||||
return mapping.mapRowSearch(ctx)
|
||||
|
@ -71,20 +80,27 @@ export default async (ctx: Ctx, next: any) => {
|
|||
let body = {}
|
||||
|
||||
switch (urlParts[0]) {
|
||||
case Resources.APPLICATION:
|
||||
case Resource.APPLICATION:
|
||||
body = processApplications(ctx)
|
||||
break
|
||||
case Resources.TABLES:
|
||||
if (urlParts[2] === Resources.ROWS) {
|
||||
case Resource.TABLES:
|
||||
if (urlParts[2] === Resource.ROWS) {
|
||||
body = processRows(ctx)
|
||||
} else {
|
||||
body = processTables(ctx)
|
||||
}
|
||||
break
|
||||
case Resources.USERS:
|
||||
case Resource.VIEWS:
|
||||
if (urlParts[2] === Resource.ROWS) {
|
||||
body = processRows(ctx)
|
||||
} else {
|
||||
body = processViews(ctx)
|
||||
}
|
||||
break
|
||||
case Resource.USERS:
|
||||
body = processUsers(ctx)
|
||||
break
|
||||
case Resources.QUERIES:
|
||||
case Resource.QUERIES:
|
||||
body = processQueries(ctx)
|
||||
break
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import controller from "../../controllers/public/rows"
|
||||
import controller, { viewSearch } from "../../controllers/public/rows"
|
||||
import Endpoint from "./utils/Endpoint"
|
||||
import { externalSearchValidator } from "../utils/validators"
|
||||
|
||||
|
@ -168,4 +168,40 @@ read.push(
|
|||
).addMiddleware(externalSearchValidator())
|
||||
)
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /views/{viewId}/rows/search:
|
||||
* post:
|
||||
* operationId: rowViewSearch
|
||||
* summary: Search for rows in a view
|
||||
* tags:
|
||||
* - rows
|
||||
* parameters:
|
||||
* - $ref: '#/components/parameters/viewId'
|
||||
* - $ref: '#/components/parameters/appId'
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/rowSearch'
|
||||
* responses:
|
||||
* 200:
|
||||
* description: The response will contain an array of rows that match the search parameters.
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/searchOutput'
|
||||
* examples:
|
||||
* search:
|
||||
* $ref: '#/components/examples/rows'
|
||||
*/
|
||||
read.push(
|
||||
new Endpoint(
|
||||
"post",
|
||||
"/views/:viewId/rows/search",
|
||||
controller.viewSearch
|
||||
).addMiddleware(externalSearchValidator())
|
||||
)
|
||||
|
||||
export default { read, write }
|
||||
|
|
|
@ -1,13 +1,24 @@
|
|||
import { User, Table, SearchFilters, Row } from "@budibase/types"
|
||||
import {
|
||||
User,
|
||||
Table,
|
||||
SearchFilters,
|
||||
Row,
|
||||
ViewV2Schema,
|
||||
ViewV2,
|
||||
ViewV2Type,
|
||||
PublicAPIView,
|
||||
} from "@budibase/types"
|
||||
import { HttpMethod, MakeRequestResponse, generateMakeRequest } from "./utils"
|
||||
import TestConfiguration from "../../../../tests/utilities/TestConfiguration"
|
||||
import { Expectations } from "../../../../tests/utilities/api/base"
|
||||
|
||||
type RequestOpts = { internal?: boolean; appId?: string }
|
||||
|
||||
type Response<T> = { data: T }
|
||||
|
||||
export interface PublicAPIExpectations {
|
||||
status?: number
|
||||
body?: Record<string, any>
|
||||
headers?: Record<string, string>
|
||||
}
|
||||
|
||||
export class PublicAPIRequest {
|
||||
|
@ -15,6 +26,7 @@ export class PublicAPIRequest {
|
|||
private appId: string | undefined
|
||||
|
||||
tables: PublicTableAPI
|
||||
views: PublicViewAPI
|
||||
rows: PublicRowAPI
|
||||
apiKey: string
|
||||
|
||||
|
@ -28,6 +40,7 @@ export class PublicAPIRequest {
|
|||
this.appId = appId
|
||||
this.tables = new PublicTableAPI(this)
|
||||
this.rows = new PublicRowAPI(this)
|
||||
this.views = new PublicViewAPI(this)
|
||||
}
|
||||
|
||||
static async init(config: TestConfiguration, user: User, opts?: RequestOpts) {
|
||||
|
@ -59,6 +72,12 @@ export class PublicAPIRequest {
|
|||
if (expectations?.body) {
|
||||
expect(res.body).toEqual(expectations?.body)
|
||||
}
|
||||
if (expectations?.headers) {
|
||||
for (let [header, value] of Object.entries(expectations.headers)) {
|
||||
const found = res.headers[header]
|
||||
expect(found?.toLowerCase()).toEqual(value)
|
||||
}
|
||||
}
|
||||
return res.body
|
||||
}
|
||||
}
|
||||
|
@ -73,9 +92,16 @@ export class PublicTableAPI {
|
|||
async create(
|
||||
table: Table,
|
||||
expectations?: PublicAPIExpectations
|
||||
): Promise<{ data: Table }> {
|
||||
): Promise<Response<Table>> {
|
||||
return this.request.send("post", "/tables", table, expectations)
|
||||
}
|
||||
|
||||
async search(
|
||||
name: string,
|
||||
expectations?: PublicAPIExpectations
|
||||
): Promise<Response<Table[]>> {
|
||||
return this.request.send("post", "/tables/search", { name }, expectations)
|
||||
}
|
||||
}
|
||||
|
||||
export class PublicRowAPI {
|
||||
|
@ -85,11 +111,24 @@ export class PublicRowAPI {
|
|||
this.request = request
|
||||
}
|
||||
|
||||
async create(
|
||||
tableId: string,
|
||||
row: Row,
|
||||
expectations?: PublicAPIExpectations
|
||||
): Promise<Response<Row>> {
|
||||
return this.request.send(
|
||||
"post",
|
||||
`/tables/${tableId}/rows`,
|
||||
row,
|
||||
expectations
|
||||
)
|
||||
}
|
||||
|
||||
async search(
|
||||
tableId: string,
|
||||
query: SearchFilters,
|
||||
expectations?: PublicAPIExpectations
|
||||
): Promise<{ data: Row[] }> {
|
||||
): Promise<Response<Row[]>> {
|
||||
return this.request.send(
|
||||
"post",
|
||||
`/tables/${tableId}/rows/search`,
|
||||
|
@ -99,4 +138,75 @@ export class PublicRowAPI {
|
|||
expectations
|
||||
)
|
||||
}
|
||||
|
||||
async viewSearch(
|
||||
viewId: string,
|
||||
query: SearchFilters,
|
||||
expectations?: PublicAPIExpectations
|
||||
): Promise<Response<Row[]>> {
|
||||
return this.request.send(
|
||||
"post",
|
||||
`/views/${viewId}/rows/search`,
|
||||
{
|
||||
query,
|
||||
},
|
||||
expectations
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export class PublicViewAPI {
|
||||
request: PublicAPIRequest
|
||||
|
||||
constructor(request: PublicAPIRequest) {
|
||||
this.request = request
|
||||
}
|
||||
|
||||
async create(
|
||||
view: Omit<PublicAPIView, "id" | "version">,
|
||||
expectations?: PublicAPIExpectations
|
||||
): Promise<Response<PublicAPIView>> {
|
||||
return this.request.send("post", "/views", view, expectations)
|
||||
}
|
||||
|
||||
async update(
|
||||
viewId: string,
|
||||
view: Omit<PublicAPIView, "id" | "version">,
|
||||
expectations?: PublicAPIExpectations
|
||||
): Promise<Response<PublicAPIView>> {
|
||||
return this.request.send("put", `/views/${viewId}`, view, expectations)
|
||||
}
|
||||
|
||||
async destroy(
|
||||
viewId: string,
|
||||
expectations?: PublicAPIExpectations
|
||||
): Promise<void> {
|
||||
return this.request.send(
|
||||
"delete",
|
||||
`/views/${viewId}`,
|
||||
undefined,
|
||||
expectations
|
||||
)
|
||||
}
|
||||
|
||||
async find(
|
||||
viewId: string,
|
||||
expectations?: PublicAPIExpectations
|
||||
): Promise<Response<PublicAPIView>> {
|
||||
return this.request.send("get", `/views/${viewId}`, undefined, expectations)
|
||||
}
|
||||
|
||||
async search(
|
||||
viewName: string,
|
||||
expectations?: PublicAPIExpectations
|
||||
): Promise<Response<PublicAPIView[]>> {
|
||||
return this.request.send(
|
||||
"post",
|
||||
"/views/search",
|
||||
{
|
||||
name: viewName,
|
||||
},
|
||||
expectations
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
import * as setup from "../../tests/utilities"
|
||||
import { PublicAPIRequest } from "./Request"
|
||||
|
||||
describe("check public API security", () => {
|
||||
const config = setup.getConfig()
|
||||
let request: PublicAPIRequest
|
||||
|
||||
beforeAll(async () => {
|
||||
await config.init()
|
||||
request = await PublicAPIRequest.init(config, await config.globalUser())
|
||||
})
|
||||
|
||||
it("should have Access-Control-Allow-Origin set to *", async () => {
|
||||
await request.tables.search("", {
|
||||
status: 200,
|
||||
headers: {
|
||||
"access-control-allow-origin": "*",
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,95 @@
|
|||
import * as setup from "../../tests/utilities"
|
||||
import { basicTable } from "../../../../tests/utilities/structures"
|
||||
import { BasicOperator, Table, UILogicalOperator } from "@budibase/types"
|
||||
import { PublicAPIRequest } from "./Request"
|
||||
import { generator } from "@budibase/backend-core/tests"
|
||||
|
||||
describe("check public API security", () => {
|
||||
const config = setup.getConfig()
|
||||
let request: PublicAPIRequest, table: Table
|
||||
|
||||
beforeAll(async () => {
|
||||
await config.init()
|
||||
request = await PublicAPIRequest.init(config, await config.globalUser())
|
||||
table = (await request.tables.create(basicTable())).data
|
||||
})
|
||||
|
||||
function baseView() {
|
||||
return {
|
||||
name: generator.word(),
|
||||
tableId: table._id!,
|
||||
query: {},
|
||||
schema: {
|
||||
name: {
|
||||
readonly: true,
|
||||
visible: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
it("should be able to create a view", async () => {
|
||||
await request.views.create(baseView(), { status: 201 })
|
||||
})
|
||||
|
||||
it("should be able to update a view", async () => {
|
||||
const view = await request.views.create(baseView(), { status: 201 })
|
||||
const response = await request.views.update(view.data.id, {
|
||||
...view.data,
|
||||
name: "new name",
|
||||
})
|
||||
})
|
||||
|
||||
it("should be able to search views", async () => {
|
||||
const viewName = "view to search for"
|
||||
const view = await request.views.create(
|
||||
{
|
||||
...baseView(),
|
||||
name: viewName,
|
||||
},
|
||||
{ status: 201 }
|
||||
)
|
||||
const results = await request.views.search(viewName, {
|
||||
status: 200,
|
||||
})
|
||||
expect(results.data.length).toEqual(1)
|
||||
expect(results.data[0].id).toEqual(view.data.id)
|
||||
})
|
||||
|
||||
it("should be able to delete a view", async () => {
|
||||
const view = await request.views.create(baseView(), { status: 201 })
|
||||
const result = await request.views.destroy(view.data.id, { status: 204 })
|
||||
expect(result).toBeDefined()
|
||||
})
|
||||
|
||||
it("should be able to search rows through a view", async () => {
|
||||
const row1 = await request.rows.create(
|
||||
table._id!,
|
||||
{ name: "hello world" },
|
||||
{ status: 200 }
|
||||
)
|
||||
await request.rows.create(table._id!, { name: "foo bar" }, { status: 200 })
|
||||
const response = await request.views.create(
|
||||
{
|
||||
...baseView(),
|
||||
query: {
|
||||
logicalOperator: UILogicalOperator.ANY,
|
||||
groups: [
|
||||
{
|
||||
filters: [
|
||||
{
|
||||
operator: BasicOperator.STRING,
|
||||
field: "name",
|
||||
value: "hello",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{ status: 201 }
|
||||
)
|
||||
const results = await request.rows.viewSearch(response.data.id, {})
|
||||
expect(results.data.length).toEqual(1)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,165 @@
|
|||
import controller from "../../controllers/public/views"
|
||||
import Endpoint from "./utils/Endpoint"
|
||||
import { viewValidator, nameValidator } from "../utils/validators"
|
||||
|
||||
const read = [],
|
||||
write = []
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /views:
|
||||
* post:
|
||||
* operationId: viewCreate
|
||||
* summary: Create a view
|
||||
* description: Create a view, this can be against an internal or external table.
|
||||
* tags:
|
||||
* - views
|
||||
* parameters:
|
||||
* - $ref: '#/components/parameters/appId'
|
||||
* requestBody:
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/view'
|
||||
* examples:
|
||||
* view:
|
||||
* $ref: '#/components/examples/view'
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Returns the created view, including the ID which has been generated for it.
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/viewOutput'
|
||||
* examples:
|
||||
* view:
|
||||
* $ref: '#/components/examples/view'
|
||||
*/
|
||||
write.push(
|
||||
new Endpoint("post", "/views", controller.create).addMiddleware(
|
||||
viewValidator()
|
||||
)
|
||||
)
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /views/{viewId}:
|
||||
* put:
|
||||
* operationId: viewUpdate
|
||||
* summary: Update a view
|
||||
* description: Update a view, this can be against an internal or external table.
|
||||
* tags:
|
||||
* - views
|
||||
* parameters:
|
||||
* - $ref: '#/components/parameters/viewId'
|
||||
* - $ref: '#/components/parameters/appId'
|
||||
* requestBody:
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/view'
|
||||
* examples:
|
||||
* view:
|
||||
* $ref: '#/components/examples/view'
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Returns the updated view.
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/viewOutput'
|
||||
* examples:
|
||||
* view:
|
||||
* $ref: '#/components/examples/view'
|
||||
*/
|
||||
write.push(
|
||||
new Endpoint("put", "/views/:viewId", controller.update).addMiddleware(
|
||||
viewValidator()
|
||||
)
|
||||
)
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /views/{viewId}:
|
||||
* delete:
|
||||
* operationId: viewDestroy
|
||||
* summary: Delete a view
|
||||
* description: Delete a view, this can be against an internal or external table.
|
||||
* tags:
|
||||
* - views
|
||||
* parameters:
|
||||
* - $ref: '#/components/parameters/viewId'
|
||||
* - $ref: '#/components/parameters/appId'
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Returns the deleted view.
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/viewOutput'
|
||||
* examples:
|
||||
* view:
|
||||
* $ref: '#/components/examples/view'
|
||||
*/
|
||||
write.push(new Endpoint("delete", "/views/:viewId", controller.destroy))
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /views/{viewId}:
|
||||
* get:
|
||||
* operationId: viewGetById
|
||||
* summary: Retrieve a view
|
||||
* description: Lookup a view, this could be internal or external.
|
||||
* tags:
|
||||
* - views
|
||||
* parameters:
|
||||
* - $ref: '#/components/parameters/viewId'
|
||||
* - $ref: '#/components/parameters/appId'
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Returns the retrieved view.
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/viewOutput'
|
||||
* examples:
|
||||
* view:
|
||||
* $ref: '#/components/examples/view'
|
||||
*/
|
||||
read.push(new Endpoint("get", "/views/:viewId", controller.read))
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /views/search:
|
||||
* post:
|
||||
* operationId: viewSearch
|
||||
* summary: Search for views
|
||||
* description: Based on view properties (currently only name) search for views.
|
||||
* tags:
|
||||
* - views
|
||||
* parameters:
|
||||
* - $ref: '#/components/parameters/appId'
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/nameSearch'
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Returns the found views, based on the search parameters.
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/viewSearch'
|
||||
* examples:
|
||||
* views:
|
||||
* $ref: '#/components/examples/views'
|
||||
*/
|
||||
read.push(
|
||||
new Endpoint("post", "/views/search", controller.search).addMiddleware(
|
||||
nameValidator()
|
||||
)
|
||||
)
|
||||
|
||||
export default { read, write }
|
|
@ -2592,6 +2592,33 @@ if (descriptions.length) {
|
|||
})
|
||||
})
|
||||
|
||||
describe("fetch", () => {
|
||||
let view: ViewV2, view2: ViewV2
|
||||
let table: Table, table2: Table
|
||||
beforeEach(async () => {
|
||||
table = await config.api.table.save(saveTableRequest())
|
||||
table2 = await config.api.table.save(saveTableRequest())
|
||||
view = await config.api.viewV2.create({
|
||||
tableId: table._id!,
|
||||
name: generator.guid(),
|
||||
schema: {},
|
||||
})
|
||||
view2 = await config.api.viewV2.create({
|
||||
tableId: table2._id!,
|
||||
name: generator.guid(),
|
||||
schema: {},
|
||||
})
|
||||
})
|
||||
|
||||
it("should be able to list views", async () => {
|
||||
const response = await config.api.viewV2.fetch({
|
||||
status: 200,
|
||||
})
|
||||
expect(response.data.find(v => v.id === view.id)).toBeDefined()
|
||||
expect(response.data.find(v => v.id === view2.id)).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("destroy", () => {
|
||||
const getRowUsage = async () => {
|
||||
const { total } = await config.doInContext(undefined, () =>
|
||||
|
|
|
@ -9,6 +9,13 @@ import {
|
|||
Table,
|
||||
WebhookActionType,
|
||||
BuiltinPermissionID,
|
||||
ViewV2Type,
|
||||
SortOrder,
|
||||
SortType,
|
||||
UILogicalOperator,
|
||||
BasicOperator,
|
||||
ArrayOperator,
|
||||
RangeOperator,
|
||||
} from "@budibase/types"
|
||||
import Joi, { CustomValidator } from "joi"
|
||||
import { ValidSnippetNameRegex, helpers } from "@budibase/shared-core"
|
||||
|
@ -66,6 +73,66 @@ export function tableValidator() {
|
|||
)
|
||||
}
|
||||
|
||||
function searchUIFilterValidator() {
|
||||
const logicalOperator = Joi.string().valid(
|
||||
...Object.values(UILogicalOperator)
|
||||
)
|
||||
const operators = [
|
||||
...Object.values(BasicOperator),
|
||||
...Object.values(ArrayOperator),
|
||||
...Object.values(RangeOperator),
|
||||
]
|
||||
const filters = Joi.array().items(
|
||||
Joi.object({
|
||||
operator: Joi.string()
|
||||
.valid(...operators)
|
||||
.required(),
|
||||
field: Joi.string().required(),
|
||||
// could do with better validation of value based on operator
|
||||
value: Joi.any().required(),
|
||||
})
|
||||
)
|
||||
return Joi.object({
|
||||
logicalOperator,
|
||||
onEmptyFilter: Joi.string().valid(...Object.values(EmptyFilterOption)),
|
||||
groups: Joi.array().items(
|
||||
Joi.object({
|
||||
logicalOperator,
|
||||
filters,
|
||||
groups: Joi.array().items(
|
||||
Joi.object({
|
||||
filters,
|
||||
logicalOperator,
|
||||
})
|
||||
),
|
||||
})
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
export function viewValidator() {
|
||||
return auth.joiValidator.body(
|
||||
Joi.object({
|
||||
id: OPTIONAL_STRING,
|
||||
tableId: Joi.string().required(),
|
||||
name: Joi.string().required(),
|
||||
type: Joi.string().optional().valid(null, ViewV2Type.CALCULATION),
|
||||
primaryDisplay: OPTIONAL_STRING,
|
||||
schema: Joi.object().required(),
|
||||
query: searchUIFilterValidator().optional(),
|
||||
sort: Joi.object({
|
||||
field: Joi.string().required(),
|
||||
order: Joi.string()
|
||||
.optional()
|
||||
.valid(...Object.values(SortOrder)),
|
||||
type: Joi.string()
|
||||
.optional()
|
||||
.valid(...Object.values(SortType)),
|
||||
}).optional(),
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
export function nameValidator() {
|
||||
return auth.joiValidator.body(
|
||||
Joi.object({
|
||||
|
@ -91,8 +158,7 @@ export function datasourceValidator() {
|
|||
)
|
||||
}
|
||||
|
||||
function filterObject(opts?: { unknown: boolean }) {
|
||||
const { unknown = true } = opts || {}
|
||||
function searchFiltersValidator() {
|
||||
const conditionalFilteringObject = () =>
|
||||
Joi.object({
|
||||
conditions: Joi.array().items(Joi.link("#schema")).required(),
|
||||
|
@ -119,7 +185,14 @@ function filterObject(opts?: { unknown: boolean }) {
|
|||
fuzzyOr: Joi.forbidden(),
|
||||
documentType: Joi.forbidden(),
|
||||
}
|
||||
return Joi.object(filtersValidators).unknown(unknown).id("schema")
|
||||
|
||||
return Joi.object(filtersValidators)
|
||||
}
|
||||
|
||||
function filterObject(opts?: { unknown: boolean }) {
|
||||
const { unknown = true } = opts || {}
|
||||
|
||||
return searchFiltersValidator().unknown(unknown).id("schema")
|
||||
}
|
||||
|
||||
export function internalSearchValidator() {
|
||||
|
|
|
@ -8,6 +8,11 @@ import { permissions } from "@budibase/backend-core"
|
|||
const router: Router = new Router()
|
||||
|
||||
router
|
||||
.get(
|
||||
"/api/v2/views",
|
||||
authorized(permissions.BUILDER),
|
||||
viewController.v2.fetch
|
||||
)
|
||||
.get(
|
||||
"/api/v2/views/:viewId",
|
||||
authorizedResource(
|
||||
|
|
|
@ -61,6 +61,9 @@ export async function run({
|
|||
inputs: ServerLogStepInputs
|
||||
appId: string
|
||||
}): Promise<ServerLogStepOutputs> {
|
||||
if (typeof inputs.text !== "string") {
|
||||
inputs.text = JSON.stringify(inputs.text)
|
||||
}
|
||||
const message = `App ${appId} - ${inputs.text}`
|
||||
console.log(message)
|
||||
return {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import * as setup from "./utilities"
|
||||
import { basicTableWithAttachmentField } from "../../tests/utilities/structures"
|
||||
import { objectStore } from "@budibase/backend-core"
|
||||
import { createAutomationBuilder } from "./utilities/AutomationTestBuilder"
|
||||
|
||||
async function uploadTestFile(filename: string) {
|
||||
let bucket = "testbucket"
|
||||
|
@ -13,6 +14,7 @@ async function uploadTestFile(filename: string) {
|
|||
|
||||
return presignedUrl
|
||||
}
|
||||
|
||||
describe("test the create row action", () => {
|
||||
let table: any
|
||||
let row: any
|
||||
|
@ -31,30 +33,78 @@ describe("test the create row action", () => {
|
|||
afterAll(setup.afterAll)
|
||||
|
||||
it("should be able to run the action", async () => {
|
||||
const res = await setup.runStep(config, setup.actions.CREATE_ROW.stepId, {
|
||||
row,
|
||||
const result = await createAutomationBuilder({
|
||||
name: "Test Create Row Flow",
|
||||
appId: config.getAppId(),
|
||||
config,
|
||||
})
|
||||
expect(res.id).toBeDefined()
|
||||
expect(res.revision).toBeDefined()
|
||||
expect(res.success).toEqual(true)
|
||||
const gottenRow = await config.api.row.get(table._id, res.id)
|
||||
.appAction({ fields: { status: "new" } })
|
||||
.serverLog({ text: "Starting create row flow" }, { stepName: "StartLog" })
|
||||
.createRow({ row }, { stepName: "CreateRow" })
|
||||
.serverLog(
|
||||
{ text: "Row created with ID: {{ stepsByName.CreateRow.row._id }}" },
|
||||
{ stepName: "CreationLog" }
|
||||
)
|
||||
.run()
|
||||
|
||||
expect(result.steps[1].outputs.success).toBeDefined()
|
||||
expect(result.steps[1].outputs.id).toBeDefined()
|
||||
expect(result.steps[1].outputs.revision).toBeDefined()
|
||||
const gottenRow = await config.api.row.get(
|
||||
table._id,
|
||||
result.steps[1].outputs.id
|
||||
)
|
||||
expect(gottenRow.name).toEqual("test")
|
||||
expect(gottenRow.description).toEqual("test")
|
||||
expect(result.steps[2].outputs.message).toContain(
|
||||
"Row created with ID: " + result.steps[1].outputs.id
|
||||
)
|
||||
})
|
||||
|
||||
it("should return an error (not throw) when bad info provided", async () => {
|
||||
const res = await setup.runStep(config, setup.actions.CREATE_ROW.stepId, {
|
||||
const result = await createAutomationBuilder({
|
||||
name: "Test Create Row Error Flow",
|
||||
appId: config.getAppId(),
|
||||
config,
|
||||
})
|
||||
.appAction({ fields: { status: "error" } })
|
||||
.serverLog({ text: "Starting error test flow" }, { stepName: "StartLog" })
|
||||
.createRow(
|
||||
{
|
||||
row: {
|
||||
tableId: "invalid",
|
||||
invalid: "invalid",
|
||||
},
|
||||
})
|
||||
expect(res.success).toEqual(false)
|
||||
},
|
||||
{ stepName: "CreateRow" }
|
||||
)
|
||||
.run()
|
||||
|
||||
expect(result.steps[1].outputs.success).toEqual(false)
|
||||
})
|
||||
|
||||
it("should check invalid inputs return an error", async () => {
|
||||
const res = await setup.runStep(config, setup.actions.CREATE_ROW.stepId, {})
|
||||
expect(res.success).toEqual(false)
|
||||
const result = await createAutomationBuilder({
|
||||
name: "Test Create Row Invalid Flow",
|
||||
appId: config.getAppId(),
|
||||
config,
|
||||
})
|
||||
.appAction({ fields: { status: "invalid" } })
|
||||
.serverLog({ text: "Testing invalid input" }, { stepName: "StartLog" })
|
||||
.createRow({ row: {} }, { stepName: "CreateRow" })
|
||||
.filter({
|
||||
field: "{{ stepsByName.CreateRow.success }}",
|
||||
condition: "equal",
|
||||
value: true,
|
||||
})
|
||||
.serverLog(
|
||||
{ text: "This log should not appear" },
|
||||
{ stepName: "SkippedLog" }
|
||||
)
|
||||
.run()
|
||||
|
||||
expect(result.steps[1].outputs.success).toEqual(false)
|
||||
expect(result.steps.length).toBeLessThan(4)
|
||||
})
|
||||
|
||||
it("should check that an attachment field is sent to storage and parsed", async () => {
|
||||
|
@ -76,13 +126,33 @@ describe("test the create row action", () => {
|
|||
]
|
||||
|
||||
attachmentRow.file_attachment = attachmentObject
|
||||
const res = await setup.runStep(config, setup.actions.CREATE_ROW.stepId, {
|
||||
row: attachmentRow,
|
||||
const result = await createAutomationBuilder({
|
||||
name: "Test Create Row Attachment Flow",
|
||||
appId: config.getAppId(),
|
||||
config,
|
||||
})
|
||||
.appAction({ fields: { type: "attachment" } })
|
||||
.serverLog(
|
||||
{ text: "Processing attachment upload" },
|
||||
{ stepName: "StartLog" }
|
||||
)
|
||||
.createRow({ row: attachmentRow }, { stepName: "CreateRow" })
|
||||
.filter({
|
||||
field: "{{ stepsByName.CreateRow.success }}",
|
||||
condition: "equal",
|
||||
value: true,
|
||||
})
|
||||
.serverLog(
|
||||
{
|
||||
text: "Attachment uploaded with key: {{ stepsByName.CreateRow.row.file_attachment.0.key }}",
|
||||
},
|
||||
{ stepName: "UploadLog" }
|
||||
)
|
||||
.run()
|
||||
|
||||
expect(res.success).toEqual(true)
|
||||
expect(res.row.file_attachment[0]).toHaveProperty("key")
|
||||
let s3Key = res.row.file_attachment[0].key
|
||||
expect(result.steps[1].outputs.success).toEqual(true)
|
||||
expect(result.steps[1].outputs.row.file_attachment[0]).toHaveProperty("key")
|
||||
let s3Key = result.steps[1].outputs.row.file_attachment[0].key
|
||||
|
||||
const client = objectStore.ObjectStore(objectStore.ObjectStoreBuckets.APPS)
|
||||
|
||||
|
@ -111,13 +181,53 @@ describe("test the create row action", () => {
|
|||
}
|
||||
|
||||
attachmentRow.single_file_attachment = attachmentObject
|
||||
const res = await setup.runStep(config, setup.actions.CREATE_ROW.stepId, {
|
||||
row: attachmentRow,
|
||||
const result = await createAutomationBuilder({
|
||||
name: "Test Create Row Single Attachment Flow",
|
||||
appId: config.getAppId(),
|
||||
config,
|
||||
})
|
||||
.appAction({ fields: { type: "single-attachment" } })
|
||||
.serverLog(
|
||||
{ text: "Processing single attachment" },
|
||||
{ stepName: "StartLog" }
|
||||
)
|
||||
.createRow({ row: attachmentRow }, { stepName: "CreateRow" })
|
||||
.branch({
|
||||
success: {
|
||||
steps: stepBuilder =>
|
||||
stepBuilder
|
||||
.serverLog(
|
||||
{ text: "Single attachment processed" },
|
||||
{ stepName: "ProcessLog" }
|
||||
)
|
||||
.serverLog(
|
||||
{
|
||||
text: "File key: {{ stepsByName.CreateRow.row.single_file_attachment.key }}",
|
||||
},
|
||||
{ stepName: "KeyLog" }
|
||||
),
|
||||
condition: {
|
||||
equal: { "{{ stepsByName.CreateRow.success }}": true },
|
||||
},
|
||||
},
|
||||
error: {
|
||||
steps: stepBuilder =>
|
||||
stepBuilder.serverLog(
|
||||
{ text: "Failed to process attachment" },
|
||||
{ stepName: "ErrorLog" }
|
||||
),
|
||||
condition: {
|
||||
equal: { "{{ stepsByName.CreateRow.success }}": false },
|
||||
},
|
||||
},
|
||||
})
|
||||
.run()
|
||||
|
||||
expect(res.success).toEqual(true)
|
||||
expect(res.row.single_file_attachment).toHaveProperty("key")
|
||||
let s3Key = res.row.single_file_attachment.key
|
||||
expect(result.steps[1].outputs.success).toEqual(true)
|
||||
expect(result.steps[1].outputs.row.single_file_attachment).toHaveProperty(
|
||||
"key"
|
||||
)
|
||||
let s3Key = result.steps[1].outputs.row.single_file_attachment.key
|
||||
|
||||
const client = objectStore.ObjectStore(objectStore.ObjectStoreBuckets.APPS)
|
||||
|
||||
|
@ -146,13 +256,50 @@ describe("test the create row action", () => {
|
|||
}
|
||||
|
||||
attachmentRow.single_file_attachment = attachmentObject
|
||||
const res = await setup.runStep(config, setup.actions.CREATE_ROW.stepId, {
|
||||
row: attachmentRow,
|
||||
const result = await createAutomationBuilder({
|
||||
name: "Test Create Row Invalid Attachment Flow",
|
||||
appId: config.getAppId(),
|
||||
config,
|
||||
})
|
||||
.appAction({ fields: { type: "invalid-attachment" } })
|
||||
.serverLog(
|
||||
{ text: "Testing invalid attachment keys" },
|
||||
{ stepName: "StartLog" }
|
||||
)
|
||||
.createRow({ row: attachmentRow }, { stepName: "CreateRow" })
|
||||
.branch({
|
||||
success: {
|
||||
steps: stepBuilder =>
|
||||
stepBuilder.serverLog(
|
||||
{ text: "Unexpected success" },
|
||||
{ stepName: "UnexpectedLog" }
|
||||
),
|
||||
condition: {
|
||||
equal: { "{{ stepsByName.CreateRow.success }}": true },
|
||||
},
|
||||
},
|
||||
error: {
|
||||
steps: stepBuilder =>
|
||||
stepBuilder
|
||||
.serverLog(
|
||||
{ text: "Expected error occurred" },
|
||||
{ stepName: "ErrorLog" }
|
||||
)
|
||||
.serverLog(
|
||||
{ text: "Error: {{ stepsByName.CreateRow.response }}" },
|
||||
{ stepName: "ErrorDetailsLog" }
|
||||
),
|
||||
condition: {
|
||||
equal: { "{{ stepsByName.CreateRow.success }}": false },
|
||||
},
|
||||
},
|
||||
})
|
||||
.run()
|
||||
|
||||
expect(res.success).toEqual(false)
|
||||
expect(res.response).toEqual(
|
||||
expect(result.steps[1].outputs.success).toEqual(false)
|
||||
expect(result.steps[1].outputs.response).toEqual(
|
||||
'Error: Attachments must have both "url" and "filename" keys. You have provided: wrongKey, anotherWrongKey'
|
||||
)
|
||||
expect(result.steps[2].outputs.status).toEqual("No branch condition met")
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,52 +1,65 @@
|
|||
import { createAutomationBuilder } from "./utilities/AutomationTestBuilder"
|
||||
import * as setup from "./utilities"
|
||||
|
||||
describe("test the delete row action", () => {
|
||||
let table: any
|
||||
let row: any
|
||||
let inputs: any
|
||||
let config = setup.getConfig()
|
||||
let table: any,
|
||||
row: any,
|
||||
config = setup.getConfig()
|
||||
|
||||
beforeEach(async () => {
|
||||
beforeAll(async () => {
|
||||
await config.init()
|
||||
table = await config.createTable()
|
||||
row = await config.createRow()
|
||||
inputs = {
|
||||
tableId: table._id,
|
||||
id: row._id,
|
||||
revision: row._rev,
|
||||
}
|
||||
})
|
||||
|
||||
afterAll(setup.afterAll)
|
||||
|
||||
it("should be able to run the action", async () => {
|
||||
const res = await setup.runStep(
|
||||
config,
|
||||
setup.actions.DELETE_ROW.stepId,
|
||||
inputs
|
||||
)
|
||||
expect(res.success).toEqual(true)
|
||||
expect(res.response).toBeDefined()
|
||||
expect(res.row._id).toEqual(row._id)
|
||||
it("should be able to run the delete row action", async () => {
|
||||
const builder = createAutomationBuilder({
|
||||
name: "Delete Row Automation",
|
||||
})
|
||||
|
||||
it("check usage quota attempts", async () => {
|
||||
await setup.runInProd(async () => {
|
||||
await setup.runStep(config, setup.actions.DELETE_ROW.stepId, inputs)
|
||||
await builder
|
||||
.appAction({ fields: {} })
|
||||
.deleteRow({
|
||||
tableId: table._id,
|
||||
id: row._id,
|
||||
revision: row._rev,
|
||||
})
|
||||
.run()
|
||||
|
||||
await config.api.row.get(table._id, row._id, {
|
||||
status: 404,
|
||||
})
|
||||
})
|
||||
|
||||
it("should check invalid inputs return an error", async () => {
|
||||
const res = await setup.runStep(config, setup.actions.DELETE_ROW.stepId, {})
|
||||
expect(res.success).toEqual(false)
|
||||
const builder = createAutomationBuilder({
|
||||
name: "Invalid Inputs Automation",
|
||||
})
|
||||
|
||||
const results = await builder
|
||||
.appAction({ fields: {} })
|
||||
.deleteRow({ tableId: "", id: "", revision: "" })
|
||||
.run()
|
||||
|
||||
expect(results.steps[0].outputs.success).toEqual(false)
|
||||
})
|
||||
|
||||
it("should return an error when table doesn't exist", async () => {
|
||||
const res = await setup.runStep(config, setup.actions.DELETE_ROW.stepId, {
|
||||
const builder = createAutomationBuilder({
|
||||
name: "Nonexistent Table Automation",
|
||||
})
|
||||
|
||||
const results = await builder
|
||||
.appAction({ fields: {} })
|
||||
.deleteRow({
|
||||
tableId: "invalid",
|
||||
id: "invalid",
|
||||
revision: "invalid",
|
||||
})
|
||||
expect(res.success).toEqual(false)
|
||||
.run()
|
||||
|
||||
expect(results.steps[0].outputs.success).toEqual(false)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,50 +1,123 @@
|
|||
import { getConfig, afterAll as _afterAll, runStep, actions } from "./utilities"
|
||||
import { createAutomationBuilder } from "./utilities/AutomationTestBuilder"
|
||||
import * as automation from "../index"
|
||||
import * as setup from "./utilities"
|
||||
import { Table } from "@budibase/types"
|
||||
|
||||
describe("test the execute script action", () => {
|
||||
let config = getConfig()
|
||||
describe("Execute Script Automations", () => {
|
||||
let config = setup.getConfig(),
|
||||
table: Table
|
||||
|
||||
beforeAll(async () => {
|
||||
beforeEach(async () => {
|
||||
await automation.init()
|
||||
await config.init()
|
||||
})
|
||||
afterAll(_afterAll)
|
||||
|
||||
it("should be able to execute a script", async () => {
|
||||
const res = await runStep(config, actions.EXECUTE_SCRIPT.stepId, {
|
||||
code: "return 1 + 1",
|
||||
})
|
||||
expect(res.value).toEqual(2)
|
||||
expect(res.success).toEqual(true)
|
||||
table = await config.createTable()
|
||||
await config.createRow()
|
||||
})
|
||||
|
||||
it("should handle a null value", async () => {
|
||||
const res = await runStep(config, actions.EXECUTE_SCRIPT.stepId, {
|
||||
code: null,
|
||||
})
|
||||
expect(res.response.message).toEqual("Invalid inputs")
|
||||
expect(res.success).toEqual(false)
|
||||
afterAll(setup.afterAll)
|
||||
|
||||
it("should execute a basic script and return the result", async () => {
|
||||
const builder = createAutomationBuilder({
|
||||
name: "Basic Script Execution",
|
||||
})
|
||||
|
||||
it("should be able to get a value from context", async () => {
|
||||
const res = await runStep(
|
||||
config,
|
||||
actions.EXECUTE_SCRIPT.stepId,
|
||||
const results = await builder
|
||||
.appAction({ fields: {} })
|
||||
.executeScript({ code: "return 2 + 2" })
|
||||
.run()
|
||||
|
||||
expect(results.steps[0].outputs.value).toEqual(4)
|
||||
})
|
||||
|
||||
it("should access bindings from previous steps", async () => {
|
||||
const builder = createAutomationBuilder({
|
||||
name: "Access Bindings",
|
||||
})
|
||||
|
||||
const results = await builder
|
||||
.appAction({ fields: { data: [1, 2, 3] } })
|
||||
.executeScript(
|
||||
{
|
||||
code: "return steps.map(d => d.value)",
|
||||
code: "return trigger.fields.data.map(x => x * 2)",
|
||||
},
|
||||
{
|
||||
steps: [{ value: 0 }, { value: 1 }],
|
||||
}
|
||||
{ stepId: "binding-script-step" }
|
||||
)
|
||||
expect(res.value).toEqual([0, 1])
|
||||
expect(res.response).toBeUndefined()
|
||||
expect(res.success).toEqual(true)
|
||||
.run()
|
||||
|
||||
expect(results.steps[0].outputs.value).toEqual([2, 4, 6])
|
||||
})
|
||||
|
||||
it("should be able to handle an error gracefully", async () => {
|
||||
const res = await runStep(config, actions.EXECUTE_SCRIPT.stepId, {
|
||||
code: "return something.map(x => x.name)",
|
||||
it("should handle script execution errors gracefully", async () => {
|
||||
const builder = createAutomationBuilder({
|
||||
name: "Handle Script Errors",
|
||||
})
|
||||
expect(res.response).toEqual("ReferenceError: something is not defined")
|
||||
expect(res.success).toEqual(false)
|
||||
|
||||
const results = await builder
|
||||
.appAction({ fields: {} })
|
||||
.executeScript({ code: "return nonexistentVariable.map(x => x)" })
|
||||
.run()
|
||||
|
||||
expect(results.steps[0].outputs.response).toContain(
|
||||
"ReferenceError: nonexistentVariable is not defined"
|
||||
)
|
||||
expect(results.steps[0].outputs.success).toEqual(false)
|
||||
})
|
||||
|
||||
it("should handle conditional logic in scripts", async () => {
|
||||
const builder = createAutomationBuilder({
|
||||
name: "Conditional Script Logic",
|
||||
})
|
||||
|
||||
const results = await builder
|
||||
.appAction({ fields: { value: 10 } })
|
||||
.executeScript({
|
||||
code: `
|
||||
if (trigger.fields.value > 5) {
|
||||
return "Value is greater than 5";
|
||||
} else {
|
||||
return "Value is 5 or less";
|
||||
}
|
||||
`,
|
||||
})
|
||||
.run()
|
||||
|
||||
expect(results.steps[0].outputs.value).toEqual("Value is greater than 5")
|
||||
})
|
||||
|
||||
it("should use multiple steps and validate script execution", async () => {
|
||||
const builder = createAutomationBuilder({
|
||||
name: "Multi-Step Script Execution",
|
||||
})
|
||||
|
||||
const results = await builder
|
||||
.appAction({ fields: {} })
|
||||
.serverLog(
|
||||
{ text: "Starting multi-step automation" },
|
||||
{ stepId: "start-log-step" }
|
||||
)
|
||||
.createRow(
|
||||
{ row: { name: "Test Row", value: 42, tableId: table._id } },
|
||||
{ stepId: "abc123" }
|
||||
)
|
||||
.executeScript(
|
||||
{
|
||||
code: `
|
||||
const createdRow = steps['abc123'];
|
||||
return createdRow.row.value * 2;
|
||||
`,
|
||||
},
|
||||
{ stepId: "ScriptingStep1" }
|
||||
)
|
||||
.serverLog({
|
||||
text: `Final result is {{ steps.ScriptingStep1.value }}`,
|
||||
})
|
||||
.run()
|
||||
|
||||
expect(results.steps[0].outputs.message).toContain(
|
||||
"Starting multi-step automation"
|
||||
)
|
||||
expect(results.steps[1].outputs.row.value).toEqual(42)
|
||||
expect(results.steps[2].outputs.value).toEqual(84)
|
||||
expect(results.steps[3].outputs.message).toContain("Final result is 84")
|
||||
})
|
||||
})
|
||||
|
|
|
@ -8,58 +8,83 @@ import {
|
|||
Table,
|
||||
TableSourceType,
|
||||
} from "@budibase/types"
|
||||
import { createAutomationBuilder } from "./utilities/AutomationTestBuilder"
|
||||
|
||||
import * as setup from "./utilities"
|
||||
import * as uuid from "uuid"
|
||||
|
||||
describe("test the update row action", () => {
|
||||
let table: Table, row: Row, inputs: any
|
||||
let config = setup.getConfig()
|
||||
let table: Table,
|
||||
row: Row,
|
||||
config = setup.getConfig()
|
||||
|
||||
beforeAll(async () => {
|
||||
await config.init()
|
||||
table = await config.createTable()
|
||||
row = await config.createRow()
|
||||
inputs = {
|
||||
rowId: row._id,
|
||||
row: {
|
||||
...row,
|
||||
name: "Updated name",
|
||||
// put a falsy option in to be removed
|
||||
description: "",
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
afterAll(setup.afterAll)
|
||||
|
||||
it("should be able to run the action", async () => {
|
||||
const res = await setup.runStep(
|
||||
config,
|
||||
setup.actions.UPDATE_ROW.stepId,
|
||||
inputs
|
||||
it("should be able to run the update row action", async () => {
|
||||
const builder = createAutomationBuilder({
|
||||
name: "Update Row Automation",
|
||||
})
|
||||
|
||||
const results = await builder
|
||||
.appAction({ fields: {} })
|
||||
.updateRow({
|
||||
rowId: row._id!,
|
||||
row: {
|
||||
...row,
|
||||
name: "Updated name",
|
||||
description: "",
|
||||
},
|
||||
meta: {},
|
||||
})
|
||||
.run()
|
||||
|
||||
expect(results.steps[0].outputs.success).toEqual(true)
|
||||
const updatedRow = await config.api.row.get(
|
||||
table._id!,
|
||||
results.steps[0].outputs.id
|
||||
)
|
||||
expect(res.success).toEqual(true)
|
||||
const updatedRow = await config.api.row.get(table._id!, res.id)
|
||||
expect(updatedRow.name).toEqual("Updated name")
|
||||
expect(updatedRow.description).not.toEqual("")
|
||||
})
|
||||
|
||||
it("should check invalid inputs return an error", async () => {
|
||||
const res = await setup.runStep(config, setup.actions.UPDATE_ROW.stepId, {})
|
||||
expect(res.success).toEqual(false)
|
||||
const builder = createAutomationBuilder({
|
||||
name: "Invalid Inputs Automation",
|
||||
})
|
||||
|
||||
const results = await builder
|
||||
.appAction({ fields: {} })
|
||||
.updateRow({ meta: {}, row: {}, rowId: "" })
|
||||
.run()
|
||||
|
||||
expect(results.steps[0].outputs.success).toEqual(false)
|
||||
})
|
||||
|
||||
it("should return an error when table doesn't exist", async () => {
|
||||
const res = await setup.runStep(config, setup.actions.UPDATE_ROW.stepId, {
|
||||
const builder = createAutomationBuilder({
|
||||
name: "Nonexistent Table Automation",
|
||||
})
|
||||
|
||||
const results = await builder
|
||||
.appAction({ fields: {} })
|
||||
.updateRow({
|
||||
row: { _id: "invalid" },
|
||||
rowId: "invalid",
|
||||
meta: {},
|
||||
})
|
||||
expect(res.success).toEqual(false)
|
||||
.run()
|
||||
|
||||
expect(results.steps[0].outputs.success).toEqual(false)
|
||||
})
|
||||
|
||||
it("should not overwrite links if those links are not set", async () => {
|
||||
let linkField: FieldSchema = {
|
||||
const linkField: FieldSchema = {
|
||||
type: FieldType.LINK,
|
||||
name: "",
|
||||
fieldName: "",
|
||||
|
@ -71,7 +96,7 @@ describe("test the update row action", () => {
|
|||
tableId: InternalTable.USER_METADATA,
|
||||
}
|
||||
|
||||
let table = await config.api.table.save({
|
||||
const table = await config.api.table.save({
|
||||
name: uuid.v4(),
|
||||
type: "table",
|
||||
sourceType: TableSourceType.INTERNAL,
|
||||
|
@ -82,23 +107,22 @@ describe("test the update row action", () => {
|
|||
},
|
||||
})
|
||||
|
||||
let user1 = await config.createUser()
|
||||
let user2 = await config.createUser()
|
||||
const user1 = await config.createUser()
|
||||
const user2 = await config.createUser()
|
||||
|
||||
let row = await config.api.row.save(table._id!, {
|
||||
const row = await config.api.row.save(table._id!, {
|
||||
user1: [{ _id: user1._id }],
|
||||
user2: [{ _id: user2._id }],
|
||||
})
|
||||
|
||||
let getResp = await config.api.row.get(table._id!, row._id!)
|
||||
expect(getResp.user1[0]._id).toEqual(user1._id)
|
||||
expect(getResp.user2[0]._id).toEqual(user2._id)
|
||||
const builder = createAutomationBuilder({
|
||||
name: "Link Preservation Automation",
|
||||
})
|
||||
|
||||
let stepResp = await setup.runStep(
|
||||
config,
|
||||
setup.actions.UPDATE_ROW.stepId,
|
||||
{
|
||||
rowId: row._id,
|
||||
const results = await builder
|
||||
.appAction({ fields: {} })
|
||||
.updateRow({
|
||||
rowId: row._id!,
|
||||
row: {
|
||||
_id: row._id,
|
||||
_rev: row._rev,
|
||||
|
@ -106,17 +130,19 @@ describe("test the update row action", () => {
|
|||
user1: [user2._id],
|
||||
user2: "",
|
||||
},
|
||||
}
|
||||
)
|
||||
expect(stepResp.success).toEqual(true)
|
||||
meta: {},
|
||||
})
|
||||
.run()
|
||||
|
||||
getResp = await config.api.row.get(table._id!, row._id!)
|
||||
expect(results.steps[0].outputs.success).toEqual(true)
|
||||
|
||||
const getResp = await config.api.row.get(table._id!, row._id!)
|
||||
expect(getResp.user1[0]._id).toEqual(user2._id)
|
||||
expect(getResp.user2[0]._id).toEqual(user2._id)
|
||||
})
|
||||
|
||||
it("should overwrite links if those links are not set and we ask it do", async () => {
|
||||
let linkField: FieldSchema = {
|
||||
it("should overwrite links if those links are not set and we ask it to", async () => {
|
||||
const linkField: FieldSchema = {
|
||||
type: FieldType.LINK,
|
||||
name: "",
|
||||
fieldName: "",
|
||||
|
@ -128,7 +154,7 @@ describe("test the update row action", () => {
|
|||
tableId: InternalTable.USER_METADATA,
|
||||
}
|
||||
|
||||
let table = await config.api.table.save({
|
||||
const table = await config.api.table.save({
|
||||
name: uuid.v4(),
|
||||
type: "table",
|
||||
sourceType: TableSourceType.INTERNAL,
|
||||
|
@ -139,23 +165,22 @@ describe("test the update row action", () => {
|
|||
},
|
||||
})
|
||||
|
||||
let user1 = await config.createUser()
|
||||
let user2 = await config.createUser()
|
||||
const user1 = await config.createUser()
|
||||
const user2 = await config.createUser()
|
||||
|
||||
let row = await config.api.row.save(table._id!, {
|
||||
const row = await config.api.row.save(table._id!, {
|
||||
user1: [{ _id: user1._id }],
|
||||
user2: [{ _id: user2._id }],
|
||||
})
|
||||
|
||||
let getResp = await config.api.row.get(table._id!, row._id!)
|
||||
expect(getResp.user1[0]._id).toEqual(user1._id)
|
||||
expect(getResp.user2[0]._id).toEqual(user2._id)
|
||||
const builder = createAutomationBuilder({
|
||||
name: "Link Overwrite Automation",
|
||||
})
|
||||
|
||||
let stepResp = await setup.runStep(
|
||||
config,
|
||||
setup.actions.UPDATE_ROW.stepId,
|
||||
{
|
||||
rowId: row._id,
|
||||
const results = await builder
|
||||
.appAction({ fields: {} })
|
||||
.updateRow({
|
||||
rowId: row._id!,
|
||||
row: {
|
||||
_id: row._id,
|
||||
_rev: row._rev,
|
||||
|
@ -170,11 +195,12 @@ describe("test the update row action", () => {
|
|||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
expect(stepResp.success).toEqual(true)
|
||||
})
|
||||
.run()
|
||||
|
||||
getResp = await config.api.row.get(table._id!, row._id!)
|
||||
expect(results.steps[0].outputs.success).toEqual(true)
|
||||
|
||||
const getResp = await config.api.row.get(table._id!, row._id!)
|
||||
expect(getResp.user1[0]._id).toEqual(user2._id)
|
||||
expect(getResp.user2).toBeUndefined()
|
||||
})
|
||||
|
|
|
@ -34,6 +34,7 @@ import {
|
|||
SearchFilters,
|
||||
Branch,
|
||||
FilterStepInputs,
|
||||
ExecuteScriptStepInputs,
|
||||
} from "@budibase/types"
|
||||
import TestConfiguration from "../../../tests/utilities/TestConfiguration"
|
||||
import * as setup from "../utilities"
|
||||
|
@ -201,6 +202,18 @@ class BaseStepBuilder {
|
|||
)
|
||||
}
|
||||
|
||||
executeScript(
|
||||
input: ExecuteScriptStepInputs,
|
||||
opts?: { stepName?: string; stepId?: string }
|
||||
): this {
|
||||
return this.step(
|
||||
AutomationActionStepId.EXECUTE_SCRIPT,
|
||||
BUILTIN_ACTION_DEFINITIONS.EXECUTE_SCRIPT,
|
||||
input,
|
||||
opts
|
||||
)
|
||||
}
|
||||
|
||||
filter(input: FilterStepInputs): this {
|
||||
return this.step(
|
||||
AutomationActionStepId.FILTER,
|
||||
|
|
|
@ -65,6 +65,9 @@ export interface paths {
|
|||
"/tables/{tableId}/rows/search": {
|
||||
post: operations["rowSearch"];
|
||||
};
|
||||
"/views/{viewId}/rows/search": {
|
||||
post: operations["rowViewSearch"];
|
||||
};
|
||||
"/tables": {
|
||||
/** Create a table, this could be internal or external. */
|
||||
post: operations["tableCreate"];
|
||||
|
@ -93,6 +96,22 @@ export interface paths {
|
|||
/** Based on user properties (currently only name) search for users. */
|
||||
post: operations["userSearch"];
|
||||
};
|
||||
"/views": {
|
||||
/** Create a view, this can be against an internal or external table. */
|
||||
post: operations["viewCreate"];
|
||||
};
|
||||
"/views/{viewId}": {
|
||||
/** Lookup a view, this could be internal or external. */
|
||||
get: operations["viewGetById"];
|
||||
/** Update a view, this can be against an internal or external table. */
|
||||
put: operations["viewUpdate"];
|
||||
/** Delete a view, this can be against an internal or external table. */
|
||||
delete: operations["viewDestroy"];
|
||||
};
|
||||
"/views/search": {
|
||||
/** Based on view properties (currently only name) search for views. */
|
||||
post: operations["viewSearch"];
|
||||
};
|
||||
}
|
||||
|
||||
export interface components {
|
||||
|
@ -813,10 +832,442 @@ export interface components {
|
|||
userIds: string[];
|
||||
};
|
||||
};
|
||||
/** @description The view to be created/updated. */
|
||||
view: {
|
||||
/** @description The name of the view. */
|
||||
name: string;
|
||||
/** @description The ID of the table this view is based on. */
|
||||
tableId: string;
|
||||
/**
|
||||
* @description The type of view - standard (empty value) or calculation.
|
||||
* @enum {string}
|
||||
*/
|
||||
type?: "calculation";
|
||||
/** @description A column used to display rows from this view - usually used when rendered in tables. */
|
||||
primaryDisplay?: string;
|
||||
/** @description Search parameters for view */
|
||||
query?: {
|
||||
/**
|
||||
* @description When using groups this defines whether all of the filters must match, or only one of them.
|
||||
* @enum {string}
|
||||
*/
|
||||
logicalOperator?: "all" | "any";
|
||||
/**
|
||||
* @description If no filters match, should the view return all rows, or no rows.
|
||||
* @enum {string}
|
||||
*/
|
||||
onEmptyFilter?: "all" | "none";
|
||||
/** @description A grouping of filters to be applied. */
|
||||
groups?: {
|
||||
/**
|
||||
* @description When using groups this defines whether all of the filters must match, or only one of them.
|
||||
* @enum {string}
|
||||
*/
|
||||
logicalOperator?: "all" | "any";
|
||||
/** @description A list of filters to apply */
|
||||
filters?: {
|
||||
/**
|
||||
* @description The type of search operation which is being performed.
|
||||
* @enum {string}
|
||||
*/
|
||||
operator?:
|
||||
| "equal"
|
||||
| "notEqual"
|
||||
| "empty"
|
||||
| "notEmpty"
|
||||
| "fuzzy"
|
||||
| "string"
|
||||
| "contains"
|
||||
| "notContains"
|
||||
| "containsAny"
|
||||
| "oneOf"
|
||||
| "range";
|
||||
/** @description The field in the view to perform the search on. */
|
||||
field?: string;
|
||||
/** @description The value to search for - the type will depend on the operator in use. */
|
||||
value?:
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| { [key: string]: unknown }
|
||||
| unknown[];
|
||||
}[];
|
||||
/** @description A grouping of filters to be applied. */
|
||||
groups?: {
|
||||
/**
|
||||
* @description When using groups this defines whether all of the filters must match, or only one of them.
|
||||
* @enum {string}
|
||||
*/
|
||||
logicalOperator?: "all" | "any";
|
||||
/** @description A list of filters to apply */
|
||||
filters?: {
|
||||
/**
|
||||
* @description The type of search operation which is being performed.
|
||||
* @enum {string}
|
||||
*/
|
||||
operator?:
|
||||
| "equal"
|
||||
| "notEqual"
|
||||
| "empty"
|
||||
| "notEmpty"
|
||||
| "fuzzy"
|
||||
| "string"
|
||||
| "contains"
|
||||
| "notContains"
|
||||
| "containsAny"
|
||||
| "oneOf"
|
||||
| "range";
|
||||
/** @description The field in the view to perform the search on. */
|
||||
field?: string;
|
||||
/** @description The value to search for - the type will depend on the operator in use. */
|
||||
value?:
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| { [key: string]: unknown }
|
||||
| unknown[];
|
||||
}[];
|
||||
}[];
|
||||
}[];
|
||||
};
|
||||
sort?: {
|
||||
/** @description The field from the table/view schema to sort on. */
|
||||
field: string;
|
||||
/**
|
||||
* @description The order in which to sort.
|
||||
* @enum {string}
|
||||
*/
|
||||
order?: "ascending" | "descending";
|
||||
/**
|
||||
* @description The type of sort to perform (by number, or by alphabetically).
|
||||
* @enum {string}
|
||||
*/
|
||||
type?: "string" | "number";
|
||||
};
|
||||
schema: {
|
||||
[key: string]:
|
||||
| {
|
||||
/** @description Defines whether the column is visible or not - rows retrieved/updated through this view will not be able to access it. */
|
||||
visible?: boolean;
|
||||
/** @description When used in combination with 'visible: true' the column will be visible in row responses but cannot be updated. */
|
||||
readonly?: boolean;
|
||||
/** @description A number defining where the column shows up in tables, lowest being first. */
|
||||
order?: number;
|
||||
/** @description A width for the column, defined in pixels - this affects rendering in tables. */
|
||||
width?: number;
|
||||
/** @description If this is a relationship column, we can set the columns we wish to include */
|
||||
column?: {
|
||||
readonly?: boolean;
|
||||
}[];
|
||||
}
|
||||
| {
|
||||
/**
|
||||
* @description This column should be built from a calculation, specifying a type and field. It is important to note when a calculation is configured all non-calculation columns will be used for grouping.
|
||||
* @enum {string}
|
||||
*/
|
||||
calculationType?: "sum" | "avg" | "count" | "min" | "max";
|
||||
/** @description The field from the table to perform the calculation on. */
|
||||
field?: string;
|
||||
/** @description Can be used in tandem with the count calculation type, to count unique entries. */
|
||||
distinct?: boolean;
|
||||
};
|
||||
};
|
||||
};
|
||||
viewOutput: {
|
||||
/** @description The view to be created/updated. */
|
||||
data: {
|
||||
/** @description The name of the view. */
|
||||
name: string;
|
||||
/** @description The ID of the table this view is based on. */
|
||||
tableId: string;
|
||||
/**
|
||||
* @description The type of view - standard (empty value) or calculation.
|
||||
* @enum {string}
|
||||
*/
|
||||
type?: "calculation";
|
||||
/** @description A column used to display rows from this view - usually used when rendered in tables. */
|
||||
primaryDisplay?: string;
|
||||
/** @description Search parameters for view */
|
||||
query?: {
|
||||
/**
|
||||
* @description When using groups this defines whether all of the filters must match, or only one of them.
|
||||
* @enum {string}
|
||||
*/
|
||||
logicalOperator?: "all" | "any";
|
||||
/**
|
||||
* @description If no filters match, should the view return all rows, or no rows.
|
||||
* @enum {string}
|
||||
*/
|
||||
onEmptyFilter?: "all" | "none";
|
||||
/** @description A grouping of filters to be applied. */
|
||||
groups?: {
|
||||
/**
|
||||
* @description When using groups this defines whether all of the filters must match, or only one of them.
|
||||
* @enum {string}
|
||||
*/
|
||||
logicalOperator?: "all" | "any";
|
||||
/** @description A list of filters to apply */
|
||||
filters?: {
|
||||
/**
|
||||
* @description The type of search operation which is being performed.
|
||||
* @enum {string}
|
||||
*/
|
||||
operator?:
|
||||
| "equal"
|
||||
| "notEqual"
|
||||
| "empty"
|
||||
| "notEmpty"
|
||||
| "fuzzy"
|
||||
| "string"
|
||||
| "contains"
|
||||
| "notContains"
|
||||
| "containsAny"
|
||||
| "oneOf"
|
||||
| "range";
|
||||
/** @description The field in the view to perform the search on. */
|
||||
field?: string;
|
||||
/** @description The value to search for - the type will depend on the operator in use. */
|
||||
value?:
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| { [key: string]: unknown }
|
||||
| unknown[];
|
||||
}[];
|
||||
/** @description A grouping of filters to be applied. */
|
||||
groups?: {
|
||||
/**
|
||||
* @description When using groups this defines whether all of the filters must match, or only one of them.
|
||||
* @enum {string}
|
||||
*/
|
||||
logicalOperator?: "all" | "any";
|
||||
/** @description A list of filters to apply */
|
||||
filters?: {
|
||||
/**
|
||||
* @description The type of search operation which is being performed.
|
||||
* @enum {string}
|
||||
*/
|
||||
operator?:
|
||||
| "equal"
|
||||
| "notEqual"
|
||||
| "empty"
|
||||
| "notEmpty"
|
||||
| "fuzzy"
|
||||
| "string"
|
||||
| "contains"
|
||||
| "notContains"
|
||||
| "containsAny"
|
||||
| "oneOf"
|
||||
| "range";
|
||||
/** @description The field in the view to perform the search on. */
|
||||
field?: string;
|
||||
/** @description The value to search for - the type will depend on the operator in use. */
|
||||
value?:
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| { [key: string]: unknown }
|
||||
| unknown[];
|
||||
}[];
|
||||
}[];
|
||||
}[];
|
||||
};
|
||||
sort?: {
|
||||
/** @description The field from the table/view schema to sort on. */
|
||||
field: string;
|
||||
/**
|
||||
* @description The order in which to sort.
|
||||
* @enum {string}
|
||||
*/
|
||||
order?: "ascending" | "descending";
|
||||
/**
|
||||
* @description The type of sort to perform (by number, or by alphabetically).
|
||||
* @enum {string}
|
||||
*/
|
||||
type?: "string" | "number";
|
||||
};
|
||||
schema: {
|
||||
[key: string]:
|
||||
| {
|
||||
/** @description Defines whether the column is visible or not - rows retrieved/updated through this view will not be able to access it. */
|
||||
visible?: boolean;
|
||||
/** @description When used in combination with 'visible: true' the column will be visible in row responses but cannot be updated. */
|
||||
readonly?: boolean;
|
||||
/** @description A number defining where the column shows up in tables, lowest being first. */
|
||||
order?: number;
|
||||
/** @description A width for the column, defined in pixels - this affects rendering in tables. */
|
||||
width?: number;
|
||||
/** @description If this is a relationship column, we can set the columns we wish to include */
|
||||
column?: {
|
||||
readonly?: boolean;
|
||||
}[];
|
||||
}
|
||||
| {
|
||||
/**
|
||||
* @description This column should be built from a calculation, specifying a type and field. It is important to note when a calculation is configured all non-calculation columns will be used for grouping.
|
||||
* @enum {string}
|
||||
*/
|
||||
calculationType?: "sum" | "avg" | "count" | "min" | "max";
|
||||
/** @description The field from the table to perform the calculation on. */
|
||||
field?: string;
|
||||
/** @description Can be used in tandem with the count calculation type, to count unique entries. */
|
||||
distinct?: boolean;
|
||||
};
|
||||
};
|
||||
/** @description The ID of the view. */
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
viewSearch: {
|
||||
data: {
|
||||
/** @description The name of the view. */
|
||||
name: string;
|
||||
/** @description The ID of the table this view is based on. */
|
||||
tableId: string;
|
||||
/**
|
||||
* @description The type of view - standard (empty value) or calculation.
|
||||
* @enum {string}
|
||||
*/
|
||||
type?: "calculation";
|
||||
/** @description A column used to display rows from this view - usually used when rendered in tables. */
|
||||
primaryDisplay?: string;
|
||||
/** @description Search parameters for view */
|
||||
query?: {
|
||||
/**
|
||||
* @description When using groups this defines whether all of the filters must match, or only one of them.
|
||||
* @enum {string}
|
||||
*/
|
||||
logicalOperator?: "all" | "any";
|
||||
/**
|
||||
* @description If no filters match, should the view return all rows, or no rows.
|
||||
* @enum {string}
|
||||
*/
|
||||
onEmptyFilter?: "all" | "none";
|
||||
/** @description A grouping of filters to be applied. */
|
||||
groups?: {
|
||||
/**
|
||||
* @description When using groups this defines whether all of the filters must match, or only one of them.
|
||||
* @enum {string}
|
||||
*/
|
||||
logicalOperator?: "all" | "any";
|
||||
/** @description A list of filters to apply */
|
||||
filters?: {
|
||||
/**
|
||||
* @description The type of search operation which is being performed.
|
||||
* @enum {string}
|
||||
*/
|
||||
operator?:
|
||||
| "equal"
|
||||
| "notEqual"
|
||||
| "empty"
|
||||
| "notEmpty"
|
||||
| "fuzzy"
|
||||
| "string"
|
||||
| "contains"
|
||||
| "notContains"
|
||||
| "containsAny"
|
||||
| "oneOf"
|
||||
| "range";
|
||||
/** @description The field in the view to perform the search on. */
|
||||
field?: string;
|
||||
/** @description The value to search for - the type will depend on the operator in use. */
|
||||
value?:
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| { [key: string]: unknown }
|
||||
| unknown[];
|
||||
}[];
|
||||
/** @description A grouping of filters to be applied. */
|
||||
groups?: {
|
||||
/**
|
||||
* @description When using groups this defines whether all of the filters must match, or only one of them.
|
||||
* @enum {string}
|
||||
*/
|
||||
logicalOperator?: "all" | "any";
|
||||
/** @description A list of filters to apply */
|
||||
filters?: {
|
||||
/**
|
||||
* @description The type of search operation which is being performed.
|
||||
* @enum {string}
|
||||
*/
|
||||
operator?:
|
||||
| "equal"
|
||||
| "notEqual"
|
||||
| "empty"
|
||||
| "notEmpty"
|
||||
| "fuzzy"
|
||||
| "string"
|
||||
| "contains"
|
||||
| "notContains"
|
||||
| "containsAny"
|
||||
| "oneOf"
|
||||
| "range";
|
||||
/** @description The field in the view to perform the search on. */
|
||||
field?: string;
|
||||
/** @description The value to search for - the type will depend on the operator in use. */
|
||||
value?:
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| { [key: string]: unknown }
|
||||
| unknown[];
|
||||
}[];
|
||||
}[];
|
||||
}[];
|
||||
};
|
||||
sort?: {
|
||||
/** @description The field from the table/view schema to sort on. */
|
||||
field: string;
|
||||
/**
|
||||
* @description The order in which to sort.
|
||||
* @enum {string}
|
||||
*/
|
||||
order?: "ascending" | "descending";
|
||||
/**
|
||||
* @description The type of sort to perform (by number, or by alphabetically).
|
||||
* @enum {string}
|
||||
*/
|
||||
type?: "string" | "number";
|
||||
};
|
||||
schema: {
|
||||
[key: string]:
|
||||
| {
|
||||
/** @description Defines whether the column is visible or not - rows retrieved/updated through this view will not be able to access it. */
|
||||
visible?: boolean;
|
||||
/** @description When used in combination with 'visible: true' the column will be visible in row responses but cannot be updated. */
|
||||
readonly?: boolean;
|
||||
/** @description A number defining where the column shows up in tables, lowest being first. */
|
||||
order?: number;
|
||||
/** @description A width for the column, defined in pixels - this affects rendering in tables. */
|
||||
width?: number;
|
||||
/** @description If this is a relationship column, we can set the columns we wish to include */
|
||||
column?: {
|
||||
readonly?: boolean;
|
||||
}[];
|
||||
}
|
||||
| {
|
||||
/**
|
||||
* @description This column should be built from a calculation, specifying a type and field. It is important to note when a calculation is configured all non-calculation columns will be used for grouping.
|
||||
* @enum {string}
|
||||
*/
|
||||
calculationType?: "sum" | "avg" | "count" | "min" | "max";
|
||||
/** @description The field from the table to perform the calculation on. */
|
||||
field?: string;
|
||||
/** @description Can be used in tandem with the count calculation type, to count unique entries. */
|
||||
distinct?: boolean;
|
||||
};
|
||||
};
|
||||
/** @description The ID of the view. */
|
||||
id: string;
|
||||
}[];
|
||||
};
|
||||
};
|
||||
parameters: {
|
||||
/** @description The ID of the table which this request is targeting. */
|
||||
tableId: string;
|
||||
/** @description The ID of the view which this request is targeting. */
|
||||
viewId: 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. */
|
||||
|
@ -1213,6 +1664,31 @@ export interface operations {
|
|||
};
|
||||
};
|
||||
};
|
||||
rowViewSearch: {
|
||||
parameters: {
|
||||
path: {
|
||||
/** The ID of the view which this request is targeting. */
|
||||
viewId: components["parameters"]["viewId"];
|
||||
};
|
||||
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": components["schemas"]["rowSearch"];
|
||||
};
|
||||
};
|
||||
};
|
||||
/** Create a table, this could be internal or external. */
|
||||
tableCreate: {
|
||||
parameters: {
|
||||
|
@ -1409,6 +1885,118 @@ export interface operations {
|
|||
};
|
||||
};
|
||||
};
|
||||
/** Create a view, this can be against an internal or external table. */
|
||||
viewCreate: {
|
||||
parameters: {
|
||||
header: {
|
||||
/** The ID of the app which this request is targeting. */
|
||||
"x-budibase-app-id": components["parameters"]["appId"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** Returns the created view, including the ID which has been generated for it. */
|
||||
200: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["viewOutput"];
|
||||
};
|
||||
};
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["view"];
|
||||
};
|
||||
};
|
||||
};
|
||||
/** Lookup a view, this could be internal or external. */
|
||||
viewGetById: {
|
||||
parameters: {
|
||||
path: {
|
||||
/** The ID of the view which this request is targeting. */
|
||||
viewId: components["parameters"]["viewId"];
|
||||
};
|
||||
header: {
|
||||
/** The ID of the app which this request is targeting. */
|
||||
"x-budibase-app-id": components["parameters"]["appId"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** Returns the retrieved view. */
|
||||
200: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["viewOutput"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/** Update a view, this can be against an internal or external table. */
|
||||
viewUpdate: {
|
||||
parameters: {
|
||||
path: {
|
||||
/** The ID of the view which this request is targeting. */
|
||||
viewId: components["parameters"]["viewId"];
|
||||
};
|
||||
header: {
|
||||
/** The ID of the app which this request is targeting. */
|
||||
"x-budibase-app-id": components["parameters"]["appId"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** Returns the updated view. */
|
||||
200: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["viewOutput"];
|
||||
};
|
||||
};
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["view"];
|
||||
};
|
||||
};
|
||||
};
|
||||
/** Delete a view, this can be against an internal or external table. */
|
||||
viewDestroy: {
|
||||
parameters: {
|
||||
path: {
|
||||
/** The ID of the view which this request is targeting. */
|
||||
viewId: components["parameters"]["viewId"];
|
||||
};
|
||||
header: {
|
||||
/** The ID of the app which this request is targeting. */
|
||||
"x-budibase-app-id": components["parameters"]["appId"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** Returns the deleted view. */
|
||||
200: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["viewOutput"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/** Based on view properties (currently only name) search for views. */
|
||||
viewSearch: {
|
||||
parameters: {
|
||||
header: {
|
||||
/** The ID of the app which this request is targeting. */
|
||||
"x-budibase-app-id": components["parameters"]["appId"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** Returns the found views, based on the search parameters. */
|
||||
200: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["viewSearch"];
|
||||
};
|
||||
};
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["nameSearch"];
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface external {}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { constants, utils } from "@budibase/backend-core"
|
||||
import { BBContext } from "@budibase/types"
|
||||
import { Ctx } from "@budibase/types"
|
||||
|
||||
export default function ({ requiresAppId }: { requiresAppId?: boolean } = {}) {
|
||||
return async (ctx: BBContext, next: any) => {
|
||||
return async (ctx: Ctx, next: any) => {
|
||||
const appId = await utils.getAppIdFromCtx(ctx)
|
||||
if (requiresAppId && !appId) {
|
||||
ctx.throw(
|
||||
|
|
|
@ -78,8 +78,11 @@ export async function getAllInternalTables(db?: Database): Promise<Table[]> {
|
|||
}
|
||||
|
||||
async function getAllExternalTables(): Promise<Table[]> {
|
||||
// this is all datasources, we'll need to filter out internal
|
||||
const datasources = await sdk.datasources.fetch({ enriched: true })
|
||||
const allEntities = datasources.map(datasource => datasource.entities)
|
||||
const allEntities = datasources
|
||||
.filter(datasource => datasource._id !== INTERNAL_TABLE_SOURCE_ID)
|
||||
.map(datasource => datasource.entities)
|
||||
let final: Table[] = []
|
||||
for (let entities of allEntities) {
|
||||
if (entities) {
|
||||
|
|
|
@ -26,6 +26,7 @@ import { isExternalTableID } from "../../../integrations/utils"
|
|||
import * as internal from "./internal"
|
||||
import * as external from "./external"
|
||||
import sdk from "../../../sdk"
|
||||
import { ensureQueryUISet } from "./utils"
|
||||
|
||||
function pickApi(tableId: any) {
|
||||
if (isExternalTableID(tableId)) {
|
||||
|
@ -44,6 +45,24 @@ export async function getEnriched(viewId: string): Promise<ViewV2Enriched> {
|
|||
return pickApi(tableId).getEnriched(viewId)
|
||||
}
|
||||
|
||||
export async function getAllEnriched(): Promise<ViewV2Enriched[]> {
|
||||
const tables = await sdk.tables.getAllTables()
|
||||
let views: ViewV2Enriched[] = []
|
||||
for (let table of tables) {
|
||||
if (!table.views || Object.keys(table.views).length === 0) {
|
||||
continue
|
||||
}
|
||||
const v2Views = Object.values(table.views).filter(isV2)
|
||||
const enrichedViews = await Promise.all(
|
||||
v2Views.map(view =>
|
||||
enrichSchema(ensureQueryUISet(view), table.schema, tables)
|
||||
)
|
||||
)
|
||||
views = views.concat(enrichedViews)
|
||||
}
|
||||
return views
|
||||
}
|
||||
|
||||
export async function getTable(view: string | ViewV2): Promise<Table> {
|
||||
const viewId = typeof view === "string" ? view : view.id
|
||||
const cached = context.getTableForView(viewId)
|
||||
|
@ -333,13 +352,19 @@ export function allowedFields(
|
|||
|
||||
export async function enrichSchema(
|
||||
view: ViewV2,
|
||||
tableSchema: TableSchema
|
||||
tableSchema: TableSchema,
|
||||
tables?: Table[]
|
||||
): Promise<ViewV2Enriched> {
|
||||
async function populateRelTableSchema(
|
||||
tableId: string,
|
||||
viewFields: Record<string, RelationSchemaField>
|
||||
) {
|
||||
const relTable = await sdk.tables.getTable(tableId)
|
||||
let relTable = tables
|
||||
? tables?.find(t => t._id === tableId)
|
||||
: await sdk.tables.getTable(tableId)
|
||||
if (!relTable) {
|
||||
throw new Error("Cannot enrich relationship, table not found")
|
||||
}
|
||||
const result: Record<string, ViewV2ColumnEnriched> = {}
|
||||
for (const relTableFieldName of Object.keys(relTable.schema)) {
|
||||
const relTableField = relTable.schema[relTableFieldName]
|
||||
|
|
|
@ -5,6 +5,7 @@ import {
|
|||
SearchViewRowRequest,
|
||||
PaginatedSearchRowResponse,
|
||||
ViewResponseEnriched,
|
||||
ViewFetchResponseEnriched,
|
||||
} from "@budibase/types"
|
||||
import { Expectations, TestAPI } from "./base"
|
||||
|
||||
|
@ -49,6 +50,12 @@ export class ViewV2API extends TestAPI {
|
|||
.data
|
||||
}
|
||||
|
||||
fetch = async (expectations?: Expectations) => {
|
||||
return await this._get<ViewFetchResponseEnriched>(`/api/v2/views`, {
|
||||
expectations,
|
||||
})
|
||||
}
|
||||
|
||||
search = async (
|
||||
viewId: string,
|
||||
params?: SearchViewRowRequest,
|
||||
|
|
|
@ -385,7 +385,7 @@ class Orchestrator {
|
|||
stepIdx: number,
|
||||
pathIdx?: number
|
||||
): Promise<number> {
|
||||
await processObject(loopStep.inputs, this.processContext(this.context))
|
||||
await processObject(loopStep.inputs, this.mergeContexts(this.context))
|
||||
const iterations = getLoopIterations(loopStep)
|
||||
let stepToLoopIndex = stepIdx + 1
|
||||
let pathStepIdx = (pathIdx || stepIdx) + 1
|
||||
|
@ -573,14 +573,14 @@ class Orchestrator {
|
|||
for (const [field, value] of Object.entries(filters[filterKey])) {
|
||||
const fromContext = processStringSync(
|
||||
field,
|
||||
this.processContext(this.context)
|
||||
this.mergeContexts(this.context)
|
||||
)
|
||||
toFilter[field] = fromContext
|
||||
|
||||
if (typeof value === "string" && findHBSBlocks(value).length > 0) {
|
||||
const processedVal = processStringSync(
|
||||
value,
|
||||
this.processContext(this.context)
|
||||
this.mergeContexts(this.context)
|
||||
)
|
||||
|
||||
filters[filterKey][field] = processedVal
|
||||
|
@ -637,7 +637,7 @@ class Orchestrator {
|
|||
const stepFn = await this.getStepFunctionality(step.stepId)
|
||||
let inputs = await processObject(
|
||||
originalStepInput,
|
||||
this.processContext(this.context)
|
||||
this.mergeContexts(this.context)
|
||||
)
|
||||
inputs = automationUtils.cleanInputValues(inputs, step.schema.inputs)
|
||||
|
||||
|
@ -645,7 +645,7 @@ class Orchestrator {
|
|||
inputs: inputs,
|
||||
appId: this.appId,
|
||||
emitter: this.emitter,
|
||||
context: this.context,
|
||||
context: this.mergeContexts(this.context),
|
||||
})
|
||||
this.handleStepOutput(step, outputs, loopIteration)
|
||||
}
|
||||
|
@ -665,8 +665,8 @@ class Orchestrator {
|
|||
return null
|
||||
}
|
||||
|
||||
private processContext(context: AutomationContext) {
|
||||
const processContext = {
|
||||
private mergeContexts(context: AutomationContext) {
|
||||
const mergeContexts = {
|
||||
...context,
|
||||
steps: {
|
||||
...context.steps,
|
||||
|
@ -674,7 +674,7 @@ class Orchestrator {
|
|||
...context.stepsByName,
|
||||
},
|
||||
}
|
||||
return processContext
|
||||
return mergeContexts
|
||||
}
|
||||
|
||||
private handleStepOutput(
|
||||
|
|
|
@ -9,6 +9,10 @@ export interface ViewResponseEnriched {
|
|||
data: ViewV2Enriched
|
||||
}
|
||||
|
||||
export interface ViewFetchResponseEnriched {
|
||||
data: ViewV2Enriched[]
|
||||
}
|
||||
|
||||
export interface CreateViewRequest extends Omit<ViewV2, "version" | "id"> {}
|
||||
|
||||
export interface UpdateViewRequest extends ViewV2 {}
|
||||
|
|
|
@ -101,6 +101,10 @@ export interface ViewV2 {
|
|||
schema?: ViewV2Schema
|
||||
}
|
||||
|
||||
export interface PublicAPIView extends Omit<ViewV2, "query" | "queryUI"> {
|
||||
query?: UISearchFilter
|
||||
}
|
||||
|
||||
export type ViewV2Schema = Record<string, ViewFieldMetadata>
|
||||
|
||||
export type ViewSchema = ViewCountOrSumSchema | ViewStatisticsSchema
|
||||
|
|
|
@ -44,7 +44,6 @@ export interface StaticUsage {
|
|||
export interface MonthlyUsage {
|
||||
[MonthlyQuotaName.QUERIES]: number
|
||||
[MonthlyQuotaName.AUTOMATIONS]: number
|
||||
[MonthlyQuotaName.DAY_PASSES]: number
|
||||
[MonthlyQuotaName.BUDIBASE_AI_CREDITS]: number
|
||||
triggers: {
|
||||
[key in MonthlyQuotaName]?: QuotaTriggers
|
||||
|
|
|
@ -62,7 +62,6 @@ export interface User extends Document {
|
|||
password?: string
|
||||
status?: UserStatus
|
||||
createdAt?: number // override the default createdAt behaviour - users sdk historically set this to Date.now()
|
||||
dayPassRecordedAt?: string
|
||||
userGroups?: string[]
|
||||
onboardedAt?: string
|
||||
freeTrialConfirmedAt?: string
|
||||
|
|
|
@ -8,7 +8,7 @@ export interface RowValue {
|
|||
export interface RowResponse<T extends Document | RowValue> {
|
||||
id: string
|
||||
key: string
|
||||
error: string
|
||||
error?: string
|
||||
value: T
|
||||
doc?: T
|
||||
}
|
||||
|
|
|
@ -163,8 +163,8 @@ export interface Database {
|
|||
viewName: string,
|
||||
params: DatabaseQueryOpts
|
||||
): Promise<AllDocsResponse<T>>
|
||||
destroy(): Promise<Nano.OkResponse | void>
|
||||
compact(): Promise<Nano.OkResponse | void>
|
||||
destroy(): Promise<Nano.OkResponse>
|
||||
compact(): Promise<Nano.OkResponse>
|
||||
// these are all PouchDB related functions that are rarely used - in future
|
||||
// should be replaced by better typed/non-pouch implemented methods
|
||||
dump(stream: Writable, opts?: DatabaseDumpOpts): Promise<any>
|
||||
|
|
|
@ -38,7 +38,6 @@ export interface AvailablePrice {
|
|||
export enum PlanModel {
|
||||
PER_USER = "perUser",
|
||||
PER_CREATOR_PER_USER = "per_creator_per_user",
|
||||
DAY_PASS = "dayPass",
|
||||
}
|
||||
|
||||
export interface PurchasedPlan {
|
||||
|
@ -49,7 +48,6 @@ export interface PurchasedPlan {
|
|||
}
|
||||
|
||||
export interface PurchasedPrice extends AvailablePrice {
|
||||
dayPasses: number | undefined
|
||||
/** @deprecated - now at the plan level via model */
|
||||
isPerUser: boolean
|
||||
}
|
||||
|
|
|
@ -23,7 +23,6 @@ export enum StaticQuotaName {
|
|||
export enum MonthlyQuotaName {
|
||||
QUERIES = "queries",
|
||||
AUTOMATIONS = "automations",
|
||||
DAY_PASSES = "dayPasses",
|
||||
BUDIBASE_AI_CREDITS = "budibaseAICredits",
|
||||
}
|
||||
|
||||
|
@ -63,7 +62,6 @@ export type PlanQuotas = { [key in PlanType]: Quotas | undefined }
|
|||
export type MonthlyQuotas = {
|
||||
[MonthlyQuotaName.QUERIES]: Quota
|
||||
[MonthlyQuotaName.AUTOMATIONS]: Quota
|
||||
[MonthlyQuotaName.DAY_PASSES]: Quota
|
||||
[MonthlyQuotaName.BUDIBASE_AI_CREDITS]: Quota
|
||||
}
|
||||
|
||||
|
|
|
@ -50,8 +50,9 @@
|
|||
"bcrypt": "5.1.0",
|
||||
"bcryptjs": "2.4.3",
|
||||
"bull": "4.10.1",
|
||||
"dd-trace": "5.23.0",
|
||||
"dd-trace": "5.26.0",
|
||||
"dotenv": "8.6.0",
|
||||
"email-validator": "^2.0.4",
|
||||
"global-agent": "3.0.0",
|
||||
"ical-generator": "4.1.0",
|
||||
"joi": "17.6.0",
|
||||
|
|
|
@ -40,6 +40,7 @@ import {
|
|||
import { checkAnyUserExists } from "../../../utilities/users"
|
||||
import { isEmailConfigured } from "../../../utilities/email"
|
||||
import { BpmStatusKey, BpmStatusValue, utils } from "@budibase/shared-core"
|
||||
import emailValidator from "email-validator"
|
||||
import crypto from "crypto"
|
||||
|
||||
const MAX_USERS_UPLOAD_LIMIT = 1000
|
||||
|
@ -300,6 +301,10 @@ export const find = async (ctx: any) => {
|
|||
|
||||
export const tenantUserLookup = async (ctx: any) => {
|
||||
const id = ctx.params.id
|
||||
// is email, check its valid
|
||||
if (id.includes("@") && !emailValidator.validate(id)) {
|
||||
ctx.throw(400, `${id} is not a valid email address to lookup.`)
|
||||
}
|
||||
const user = await userSdk.core.getFirstPlatformUser(id)
|
||||
if (user) {
|
||||
ctx.body = user
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue