commit
a385fde601
|
@ -8,8 +8,8 @@ services:
|
||||||
# Last version that supports the "fs" backend
|
# Last version that supports the "fs" backend
|
||||||
image: minio/minio:RELEASE.2022-10-24T18-35-07Z
|
image: minio/minio:RELEASE.2022-10-24T18-35-07Z
|
||||||
ports:
|
ports:
|
||||||
- 9000
|
- "9000"
|
||||||
- 9001
|
- "9001"
|
||||||
environment:
|
environment:
|
||||||
MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY}
|
MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY}
|
||||||
MINIO_SECRET_KEY: ${MINIO_SECRET_KEY}
|
MINIO_SECRET_KEY: ${MINIO_SECRET_KEY}
|
||||||
|
@ -28,9 +28,9 @@ services:
|
||||||
- COUCHDB_PASSWORD=${COUCH_DB_PASSWORD}
|
- COUCHDB_PASSWORD=${COUCH_DB_PASSWORD}
|
||||||
- COUCHDB_USER=${COUCH_DB_USER}
|
- COUCHDB_USER=${COUCH_DB_USER}
|
||||||
ports:
|
ports:
|
||||||
- 5984
|
- "5984"
|
||||||
- 4369
|
- "4369"
|
||||||
- 9100
|
- "9100"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:5984/_up"]
|
test: ["CMD", "curl", "-f", "http://localhost:5984/_up"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
|
@ -42,6 +42,6 @@ services:
|
||||||
image: redis
|
image: redis
|
||||||
command: redis-server --requirepass ${REDIS_PASSWORD}
|
command: redis-server --requirepass ${REDIS_PASSWORD}
|
||||||
ports:
|
ports:
|
||||||
- 6379
|
- "6379"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "redis-cli", "ping"]
|
test: ["CMD", "redis-cli", "ping"]
|
|
@ -13,7 +13,7 @@
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"kill-port": "^1.6.1",
|
"kill-port": "^1.6.1",
|
||||||
"lerna": "3.14.1",
|
"lerna": "3.14.1",
|
||||||
"madge": "^5.0.1",
|
"madge": "^6.0.0",
|
||||||
"prettier": "^2.3.1",
|
"prettier": "^2.3.1",
|
||||||
"prettier-plugin-svelte": "^2.3.0",
|
"prettier-plugin-svelte": "^2.3.0",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
|
@ -84,4 +84,4 @@
|
||||||
"install:pro": "bash scripts/pro/install.sh",
|
"install:pro": "bash scripts/pro/install.sh",
|
||||||
"dep:clean": "yarn clean && yarn bootstrap"
|
"dep:clean": "yarn clean && yarn bootstrap"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -227,6 +227,6 @@ export async function platformLogout(opts: PlatformLogoutOpts) {
|
||||||
|
|
||||||
const sessionIds = sessions.map(({ sessionId }) => sessionId)
|
const sessionIds = sessions.map(({ sessionId }) => sessionId)
|
||||||
await invalidateSessions(userId, { sessionIds, reason: "logout" })
|
await invalidateSessions(userId, { sessionIds, reason: "logout" })
|
||||||
await events.auth.logout()
|
await events.auth.logout(ctx.user?.email)
|
||||||
await userCache.invalidateUser(userId)
|
await userCache.invalidateUser(userId)
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,6 +68,7 @@ export enum DocumentType {
|
||||||
MEM_VIEW = "view",
|
MEM_VIEW = "view",
|
||||||
USER_FLAG = "flag",
|
USER_FLAG = "flag",
|
||||||
AUTOMATION_METADATA = "meta_au",
|
AUTOMATION_METADATA = "meta_au",
|
||||||
|
AUDIT_LOG = "al",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StaticDatabases = {
|
export const StaticDatabases = {
|
||||||
|
@ -88,6 +89,9 @@ export const StaticDatabases = {
|
||||||
install: "install",
|
install: "install",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
AUDIT_LOGS: {
|
||||||
|
name: "audit-logs",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const APP_PREFIX = DocumentType.APP + SEPARATOR
|
export const APP_PREFIX = DocumentType.APP + SEPARATOR
|
||||||
|
|
|
@ -41,5 +41,6 @@ export enum Config {
|
||||||
OIDC_LOGOS = "logos_oidc",
|
OIDC_LOGOS = "logos_oidc",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const MIN_VALID_DATE = new Date(-2147483647000)
|
||||||
export const MAX_VALID_DATE = new Date(2147483647000)
|
export const MAX_VALID_DATE = new Date(2147483647000)
|
||||||
export const DEFAULT_TENANT_ID = "default"
|
export const DEFAULT_TENANT_ID = "default"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { AsyncLocalStorage } from "async_hooks"
|
import { AsyncLocalStorage } from "async_hooks"
|
||||||
import { ContextMap } from "./mainContext"
|
import { ContextMap } from "./types"
|
||||||
|
|
||||||
export default class Context {
|
export default class Context {
|
||||||
static storage = new AsyncLocalStorage<ContextMap>()
|
static storage = new AsyncLocalStorage<ContextMap>()
|
||||||
|
|
|
@ -5,6 +5,8 @@ import {
|
||||||
isCloudAccount,
|
isCloudAccount,
|
||||||
Account,
|
Account,
|
||||||
AccountUserContext,
|
AccountUserContext,
|
||||||
|
UserContext,
|
||||||
|
Ctx,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import * as context from "."
|
import * as context from "."
|
||||||
|
|
||||||
|
@ -16,15 +18,22 @@ export function doInIdentityContext(identity: IdentityContext, task: any) {
|
||||||
return context.doInIdentityContext(identity, task)
|
return context.doInIdentityContext(identity, task)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function doInUserContext(user: User, task: any) {
|
// used in server/worker
|
||||||
const userContext: any = {
|
export function doInUserContext(user: User, ctx: Ctx, task: any) {
|
||||||
|
const userContext: UserContext = {
|
||||||
...user,
|
...user,
|
||||||
_id: user._id as string,
|
_id: user._id as string,
|
||||||
type: IdentityType.USER,
|
type: IdentityType.USER,
|
||||||
|
hostInfo: {
|
||||||
|
ipAddress: ctx.request.ip,
|
||||||
|
// filled in by koa-useragent package
|
||||||
|
userAgent: ctx.userAgent._agent.source,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
return doInIdentityContext(userContext, task)
|
return doInIdentityContext(userContext, task)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// used in account portal
|
||||||
export function doInAccountContext(account: Account, task: any) {
|
export function doInAccountContext(account: Account, task: any) {
|
||||||
const _id = getAccountUserId(account)
|
const _id = getAccountUserId(account)
|
||||||
const tenantId = account.tenantId
|
const tenantId = account.tenantId
|
||||||
|
|
|
@ -11,13 +11,7 @@ import {
|
||||||
DEFAULT_TENANT_ID,
|
DEFAULT_TENANT_ID,
|
||||||
} from "../constants"
|
} from "../constants"
|
||||||
import { Database, IdentityContext } from "@budibase/types"
|
import { Database, IdentityContext } from "@budibase/types"
|
||||||
|
import { ContextMap } from "./types"
|
||||||
export type ContextMap = {
|
|
||||||
tenantId?: string
|
|
||||||
appId?: string
|
|
||||||
identity?: IdentityContext
|
|
||||||
environmentVariables?: Record<string, string>
|
|
||||||
}
|
|
||||||
|
|
||||||
let TEST_APP_ID: string | null = null
|
let TEST_APP_ID: string | null = null
|
||||||
|
|
||||||
|
@ -30,14 +24,23 @@ export function getGlobalDBName(tenantId?: string) {
|
||||||
return baseGlobalDBName(tenantId)
|
return baseGlobalDBName(tenantId)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function baseGlobalDBName(tenantId: string | undefined | null) {
|
export function getAuditLogDBName(tenantId?: string) {
|
||||||
let dbName
|
if (!tenantId) {
|
||||||
if (!tenantId || tenantId === DEFAULT_TENANT_ID) {
|
tenantId = getTenantId()
|
||||||
dbName = StaticDatabases.GLOBAL.name
|
}
|
||||||
} else {
|
if (tenantId === DEFAULT_TENANT_ID) {
|
||||||
dbName = `${tenantId}${SEPARATOR}${StaticDatabases.GLOBAL.name}`
|
return StaticDatabases.AUDIT_LOGS.name
|
||||||
|
} else {
|
||||||
|
return `${tenantId}${SEPARATOR}${StaticDatabases.AUDIT_LOGS.name}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function baseGlobalDBName(tenantId: string | undefined | null) {
|
||||||
|
if (!tenantId || tenantId === DEFAULT_TENANT_ID) {
|
||||||
|
return StaticDatabases.GLOBAL.name
|
||||||
|
} else {
|
||||||
|
return `${tenantId}${SEPARATOR}${StaticDatabases.GLOBAL.name}`
|
||||||
}
|
}
|
||||||
return dbName
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isMultiTenant() {
|
export function isMultiTenant() {
|
||||||
|
@ -228,6 +231,13 @@ export function getGlobalDB(): Database {
|
||||||
return getDB(baseGlobalDBName(context?.tenantId))
|
return getDB(baseGlobalDBName(context?.tenantId))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getAuditLogsDB(): Database {
|
||||||
|
if (!getTenantId()) {
|
||||||
|
throw new Error("No tenant ID found - cannot open audit log DB")
|
||||||
|
}
|
||||||
|
return getDB(getAuditLogDBName())
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the app database based on whatever the request
|
* Gets the app database based on whatever the request
|
||||||
* contained, dev or prod.
|
* contained, dev or prod.
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { IdentityContext } from "@budibase/types"
|
||||||
|
|
||||||
|
// keep this out of Budibase types, don't want to expose context info
|
||||||
|
export type ContextMap = {
|
||||||
|
tenantId?: string
|
||||||
|
appId?: string
|
||||||
|
identity?: IdentityContext
|
||||||
|
environmentVariables?: Record<string, string>
|
||||||
|
}
|
|
@ -7,3 +7,4 @@ export { default as Replication } from "./Replication"
|
||||||
// exports to support old export structure
|
// exports to support old export structure
|
||||||
export * from "../constants/db"
|
export * from "../constants/db"
|
||||||
export { getGlobalDBName, baseGlobalDBName } from "../context"
|
export { getGlobalDBName, baseGlobalDBName } from "../context"
|
||||||
|
export * from "./lucene"
|
||||||
|
|
|
@ -0,0 +1,624 @@
|
||||||
|
import fetch from "node-fetch"
|
||||||
|
import { getCouchInfo } from "./couch"
|
||||||
|
import { SearchFilters, Row } from "@budibase/types"
|
||||||
|
|
||||||
|
const QUERY_START_REGEX = /\d[0-9]*:/g
|
||||||
|
|
||||||
|
interface SearchResponse<T> {
|
||||||
|
rows: T[] | any[]
|
||||||
|
bookmark: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PaginatedSearchResponse<T> extends SearchResponse<T> {
|
||||||
|
hasNextPage: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SearchParams<T> = {
|
||||||
|
tableId?: string
|
||||||
|
sort?: string
|
||||||
|
sortOrder?: string
|
||||||
|
sortType?: string
|
||||||
|
limit?: number
|
||||||
|
bookmark?: string
|
||||||
|
version?: string
|
||||||
|
indexer?: () => Promise<any>
|
||||||
|
disableEscaping?: boolean
|
||||||
|
rows?: T | Row[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeKeyNumbering(key: any): string {
|
||||||
|
if (typeof key === "string" && key.match(QUERY_START_REGEX) != null) {
|
||||||
|
const parts = key.split(":")
|
||||||
|
// remove the number
|
||||||
|
parts.shift()
|
||||||
|
return parts.join(":")
|
||||||
|
} else {
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class to build lucene query URLs.
|
||||||
|
* Optionally takes a base lucene query object.
|
||||||
|
*/
|
||||||
|
export class QueryBuilder<T> {
|
||||||
|
dbName: string
|
||||||
|
index: string
|
||||||
|
query: SearchFilters
|
||||||
|
limit: number
|
||||||
|
sort?: string
|
||||||
|
bookmark?: string
|
||||||
|
sortOrder: string
|
||||||
|
sortType: string
|
||||||
|
includeDocs: boolean
|
||||||
|
version?: string
|
||||||
|
indexBuilder?: () => Promise<any>
|
||||||
|
noEscaping = false
|
||||||
|
|
||||||
|
constructor(dbName: string, index: string, base?: SearchFilters) {
|
||||||
|
this.dbName = dbName
|
||||||
|
this.index = index
|
||||||
|
this.query = {
|
||||||
|
allOr: false,
|
||||||
|
string: {},
|
||||||
|
fuzzy: {},
|
||||||
|
range: {},
|
||||||
|
equal: {},
|
||||||
|
notEqual: {},
|
||||||
|
empty: {},
|
||||||
|
notEmpty: {},
|
||||||
|
oneOf: {},
|
||||||
|
contains: {},
|
||||||
|
notContains: {},
|
||||||
|
containsAny: {},
|
||||||
|
...base,
|
||||||
|
}
|
||||||
|
this.limit = 50
|
||||||
|
this.sortOrder = "ascending"
|
||||||
|
this.sortType = "string"
|
||||||
|
this.includeDocs = true
|
||||||
|
}
|
||||||
|
|
||||||
|
disableEscaping() {
|
||||||
|
this.noEscaping = true
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
setIndexBuilder(builderFn: () => Promise<any>) {
|
||||||
|
this.indexBuilder = builderFn
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
setVersion(version?: string) {
|
||||||
|
if (version != null) {
|
||||||
|
this.version = version
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
setTable(tableId: string) {
|
||||||
|
this.query.equal!.tableId = tableId
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
setLimit(limit?: number) {
|
||||||
|
if (limit != null) {
|
||||||
|
this.limit = limit
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
setSort(sort?: string) {
|
||||||
|
if (sort != null) {
|
||||||
|
this.sort = sort
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
setSortOrder(sortOrder?: string) {
|
||||||
|
if (sortOrder != null) {
|
||||||
|
this.sortOrder = sortOrder
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
setSortType(sortType?: string) {
|
||||||
|
if (sortType != null) {
|
||||||
|
this.sortType = sortType
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
setBookmark(bookmark?: string) {
|
||||||
|
if (bookmark != null) {
|
||||||
|
this.bookmark = bookmark
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
excludeDocs() {
|
||||||
|
this.includeDocs = false
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
addString(key: string, partial: string) {
|
||||||
|
this.query.string![key] = partial
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
addFuzzy(key: string, fuzzy: string) {
|
||||||
|
this.query.fuzzy![key] = fuzzy
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
addRange(key: string, low: string | number, high: string | number) {
|
||||||
|
this.query.range![key] = {
|
||||||
|
low,
|
||||||
|
high,
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
addEqual(key: string, value: any) {
|
||||||
|
this.query.equal![key] = value
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
addNotEqual(key: string, value: any) {
|
||||||
|
this.query.notEqual![key] = value
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
addEmpty(key: string, value: any) {
|
||||||
|
this.query.empty![key] = value
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
addNotEmpty(key: string, value: any) {
|
||||||
|
this.query.notEmpty![key] = value
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
addOneOf(key: string, value: any) {
|
||||||
|
this.query.oneOf![key] = value
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
addContains(key: string, value: any) {
|
||||||
|
this.query.contains![key] = value
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
addNotContains(key: string, value: any) {
|
||||||
|
this.query.notContains![key] = value
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
addContainsAny(key: string, value: any) {
|
||||||
|
this.query.containsAny![key] = value
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSpaces(input: string) {
|
||||||
|
if (this.noEscaping) {
|
||||||
|
return input
|
||||||
|
} else {
|
||||||
|
return input.replace(/ /g, "_")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preprocesses a value before going into a lucene search.
|
||||||
|
* Transforms strings to lowercase and wraps strings and bools in quotes.
|
||||||
|
* @param value The value to process
|
||||||
|
* @param options The preprocess options
|
||||||
|
* @returns {string|*}
|
||||||
|
*/
|
||||||
|
preprocess(value: any, { escape, lowercase, wrap, type }: any = {}) {
|
||||||
|
const hasVersion = !!this.version
|
||||||
|
// Determine if type needs wrapped
|
||||||
|
const originalType = typeof value
|
||||||
|
// Convert to lowercase
|
||||||
|
if (value && lowercase) {
|
||||||
|
value = value.toLowerCase ? value.toLowerCase() : value
|
||||||
|
}
|
||||||
|
// Escape characters
|
||||||
|
if (!this.noEscaping && escape && originalType === "string") {
|
||||||
|
value = `${value}`.replace(/[ #+\-&|!(){}\]^"~*?:\\]/g, "\\$&")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap in quotes
|
||||||
|
if (originalType === "string" && !isNaN(value) && !type) {
|
||||||
|
value = `"${value}"`
|
||||||
|
} else if (hasVersion && wrap) {
|
||||||
|
value = originalType === "number" ? value : `"${value}"`
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
buildSearchQuery() {
|
||||||
|
const builder = this
|
||||||
|
let allOr = this.query && this.query.allOr
|
||||||
|
let query = allOr ? "" : "*:*"
|
||||||
|
const allPreProcessingOpts = { escape: true, lowercase: true, wrap: true }
|
||||||
|
let tableId
|
||||||
|
if (this.query.equal!.tableId) {
|
||||||
|
tableId = this.query.equal!.tableId
|
||||||
|
delete this.query.equal!.tableId
|
||||||
|
}
|
||||||
|
|
||||||
|
const equal = (key: string, value: any) => {
|
||||||
|
// 0 evaluates to false, which means we would return all rows if we don't check it
|
||||||
|
if (!value && value !== 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return `${key}:${builder.preprocess(value, allPreProcessingOpts)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const contains = (key: string, value: any, mode = "AND") => {
|
||||||
|
if (Array.isArray(value) && value.length === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (!Array.isArray(value)) {
|
||||||
|
return `${key}:${value}`
|
||||||
|
}
|
||||||
|
let statement = `${builder.preprocess(value[0], { escape: true })}`
|
||||||
|
for (let i = 1; i < value.length; i++) {
|
||||||
|
statement += ` ${mode} ${builder.preprocess(value[i], {
|
||||||
|
escape: true,
|
||||||
|
})}`
|
||||||
|
}
|
||||||
|
return `${key}:(${statement})`
|
||||||
|
}
|
||||||
|
|
||||||
|
const notContains = (key: string, value: any) => {
|
||||||
|
// @ts-ignore
|
||||||
|
const allPrefix = allOr === "" ? "*:* AND" : ""
|
||||||
|
return allPrefix + "NOT " + contains(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const containsAny = (key: string, value: any) => {
|
||||||
|
return contains(key, value, "OR")
|
||||||
|
}
|
||||||
|
|
||||||
|
const oneOf = (key: string, value: any) => {
|
||||||
|
if (!Array.isArray(value)) {
|
||||||
|
if (typeof value === "string") {
|
||||||
|
value = value.split(",")
|
||||||
|
} else {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let orStatement = `${builder.preprocess(value[0], allPreProcessingOpts)}`
|
||||||
|
for (let i = 1; i < value.length; i++) {
|
||||||
|
orStatement += ` OR ${builder.preprocess(
|
||||||
|
value[i],
|
||||||
|
allPreProcessingOpts
|
||||||
|
)}`
|
||||||
|
}
|
||||||
|
return `${key}:(${orStatement})`
|
||||||
|
}
|
||||||
|
|
||||||
|
function build(structure: any, queryFn: any) {
|
||||||
|
for (let [key, value] of Object.entries(structure)) {
|
||||||
|
// check for new format - remove numbering if needed
|
||||||
|
key = removeKeyNumbering(key)
|
||||||
|
key = builder.preprocess(builder.handleSpaces(key), {
|
||||||
|
escape: true,
|
||||||
|
})
|
||||||
|
const expression = queryFn(key, value)
|
||||||
|
if (expression == null) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (query.length > 0) {
|
||||||
|
query += ` ${allOr ? "OR" : "AND"} `
|
||||||
|
}
|
||||||
|
query += expression
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct the actual lucene search query string from JSON structure
|
||||||
|
if (this.query.string) {
|
||||||
|
build(this.query.string, (key: string, value: any) => {
|
||||||
|
if (!value) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
value = builder.preprocess(value, {
|
||||||
|
escape: true,
|
||||||
|
lowercase: true,
|
||||||
|
type: "string",
|
||||||
|
})
|
||||||
|
return `${key}:${value}*`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (this.query.range) {
|
||||||
|
build(this.query.range, (key: string, value: any) => {
|
||||||
|
if (!value) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (value.low == null || value.low === "") {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (value.high == null || value.high === "") {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const low = builder.preprocess(value.low, allPreProcessingOpts)
|
||||||
|
const high = builder.preprocess(value.high, allPreProcessingOpts)
|
||||||
|
return `${key}:[${low} TO ${high}]`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (this.query.fuzzy) {
|
||||||
|
build(this.query.fuzzy, (key: string, value: any) => {
|
||||||
|
if (!value) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
value = builder.preprocess(value, {
|
||||||
|
escape: true,
|
||||||
|
lowercase: true,
|
||||||
|
type: "fuzzy",
|
||||||
|
})
|
||||||
|
return `${key}:${value}~`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (this.query.equal) {
|
||||||
|
build(this.query.equal, equal)
|
||||||
|
}
|
||||||
|
if (this.query.notEqual) {
|
||||||
|
build(this.query.notEqual, (key: string, value: any) => {
|
||||||
|
if (!value) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return `!${key}:${builder.preprocess(value, allPreProcessingOpts)}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (this.query.empty) {
|
||||||
|
build(this.query.empty, (key: string) => `!${key}:["" TO *]`)
|
||||||
|
}
|
||||||
|
if (this.query.notEmpty) {
|
||||||
|
build(this.query.notEmpty, (key: string) => `${key}:["" TO *]`)
|
||||||
|
}
|
||||||
|
if (this.query.oneOf) {
|
||||||
|
build(this.query.oneOf, oneOf)
|
||||||
|
}
|
||||||
|
if (this.query.contains) {
|
||||||
|
build(this.query.contains, contains)
|
||||||
|
}
|
||||||
|
if (this.query.notContains) {
|
||||||
|
build(this.query.notContains, notContains)
|
||||||
|
}
|
||||||
|
if (this.query.containsAny) {
|
||||||
|
build(this.query.containsAny, containsAny)
|
||||||
|
}
|
||||||
|
// make sure table ID is always added as an AND
|
||||||
|
if (tableId) {
|
||||||
|
query = `(${query})`
|
||||||
|
allOr = false
|
||||||
|
build({ tableId }, equal)
|
||||||
|
}
|
||||||
|
return query
|
||||||
|
}
|
||||||
|
|
||||||
|
buildSearchBody() {
|
||||||
|
let body: any = {
|
||||||
|
q: this.buildSearchQuery(),
|
||||||
|
limit: Math.min(this.limit, 200),
|
||||||
|
include_docs: this.includeDocs,
|
||||||
|
}
|
||||||
|
if (this.bookmark) {
|
||||||
|
body.bookmark = this.bookmark
|
||||||
|
}
|
||||||
|
if (this.sort) {
|
||||||
|
const order = this.sortOrder === "descending" ? "-" : ""
|
||||||
|
const type = `<${this.sortType}>`
|
||||||
|
body.sort = `${order}${this.handleSpaces(this.sort)}${type}`
|
||||||
|
}
|
||||||
|
return body
|
||||||
|
}
|
||||||
|
|
||||||
|
async run() {
|
||||||
|
const { url, cookie } = getCouchInfo()
|
||||||
|
const fullPath = `${url}/${this.dbName}/_design/database/_search/${this.index}`
|
||||||
|
const body = this.buildSearchBody()
|
||||||
|
try {
|
||||||
|
return await runQuery<T>(fullPath, body, cookie)
|
||||||
|
} catch (err: any) {
|
||||||
|
if (err.status === 404 && this.indexBuilder) {
|
||||||
|
await this.indexBuilder()
|
||||||
|
return await runQuery<T>(fullPath, body, cookie)
|
||||||
|
} else {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes a lucene search query.
|
||||||
|
* @param url The query URL
|
||||||
|
* @param body The request body defining search criteria
|
||||||
|
* @param cookie The auth cookie for CouchDB
|
||||||
|
* @returns {Promise<{rows: []}>}
|
||||||
|
*/
|
||||||
|
async function runQuery<T>(
|
||||||
|
url: string,
|
||||||
|
body: any,
|
||||||
|
cookie: string
|
||||||
|
): Promise<SearchResponse<T>> {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: cookie,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.status === 404) {
|
||||||
|
throw response
|
||||||
|
}
|
||||||
|
const json = await response.json()
|
||||||
|
|
||||||
|
let output: any = {
|
||||||
|
rows: [],
|
||||||
|
}
|
||||||
|
if (json.rows != null && json.rows.length > 0) {
|
||||||
|
output.rows = json.rows.map((row: any) => row.doc)
|
||||||
|
}
|
||||||
|
if (json.bookmark) {
|
||||||
|
output.bookmark = json.bookmark
|
||||||
|
}
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets round the fixed limit of 200 results from a query by fetching as many
|
||||||
|
* pages as required and concatenating the results. This recursively operates
|
||||||
|
* until enough results have been found.
|
||||||
|
* @param dbName {string} Which database to run a lucene query on
|
||||||
|
* @param index {string} Which search index to utilise
|
||||||
|
* @param query {object} The JSON query structure
|
||||||
|
* @param params {object} The search params including:
|
||||||
|
* tableId {string} The table ID to search
|
||||||
|
* sort {string} The sort column
|
||||||
|
* sortOrder {string} The sort order ("ascending" or "descending")
|
||||||
|
* sortType {string} Whether to treat sortable values as strings or
|
||||||
|
* numbers. ("string" or "number")
|
||||||
|
* limit {number} The number of results to fetch
|
||||||
|
* bookmark {string|null} Current bookmark in the recursive search
|
||||||
|
* rows {array|null} Current results in the recursive search
|
||||||
|
* @returns {Promise<*[]|*>}
|
||||||
|
*/
|
||||||
|
async function recursiveSearch<T>(
|
||||||
|
dbName: string,
|
||||||
|
index: string,
|
||||||
|
query: any,
|
||||||
|
params: any
|
||||||
|
): Promise<any> {
|
||||||
|
const bookmark = params.bookmark
|
||||||
|
const rows = params.rows || []
|
||||||
|
if (rows.length >= params.limit) {
|
||||||
|
return rows
|
||||||
|
}
|
||||||
|
let pageSize = 200
|
||||||
|
if (rows.length > params.limit - 200) {
|
||||||
|
pageSize = params.limit - rows.length
|
||||||
|
}
|
||||||
|
const page = await new QueryBuilder<T>(dbName, index, query)
|
||||||
|
.setVersion(params.version)
|
||||||
|
.setTable(params.tableId)
|
||||||
|
.setBookmark(bookmark)
|
||||||
|
.setLimit(pageSize)
|
||||||
|
.setSort(params.sort)
|
||||||
|
.setSortOrder(params.sortOrder)
|
||||||
|
.setSortType(params.sortType)
|
||||||
|
.run()
|
||||||
|
if (!page.rows.length) {
|
||||||
|
return rows
|
||||||
|
}
|
||||||
|
if (page.rows.length < 200) {
|
||||||
|
return [...rows, ...page.rows]
|
||||||
|
}
|
||||||
|
const newParams = {
|
||||||
|
...params,
|
||||||
|
bookmark: page.bookmark,
|
||||||
|
rows: [...rows, ...page.rows],
|
||||||
|
}
|
||||||
|
return await recursiveSearch(dbName, index, query, newParams)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs a paginated search. A bookmark will be returned to allow the next
|
||||||
|
* page to be fetched. There is a max limit off 200 results per page in a
|
||||||
|
* paginated search.
|
||||||
|
* @param dbName {string} Which database to run a lucene query on
|
||||||
|
* @param index {string} Which search index to utilise
|
||||||
|
* @param query {object} The JSON query structure
|
||||||
|
* @param params {object} The search params including:
|
||||||
|
* tableId {string} The table ID to search
|
||||||
|
* sort {string} The sort column
|
||||||
|
* sortOrder {string} The sort order ("ascending" or "descending")
|
||||||
|
* sortType {string} Whether to treat sortable values as strings or
|
||||||
|
* numbers. ("string" or "number")
|
||||||
|
* limit {number} The desired page size
|
||||||
|
* bookmark {string} The bookmark to resume from
|
||||||
|
* @returns {Promise<{hasNextPage: boolean, rows: *[]}>}
|
||||||
|
*/
|
||||||
|
export async function paginatedSearch<T>(
|
||||||
|
dbName: string,
|
||||||
|
index: string,
|
||||||
|
query: SearchFilters,
|
||||||
|
params: SearchParams<T>
|
||||||
|
) {
|
||||||
|
let limit = params.limit
|
||||||
|
if (limit == null || isNaN(limit) || limit < 0) {
|
||||||
|
limit = 50
|
||||||
|
}
|
||||||
|
limit = Math.min(limit, 200)
|
||||||
|
const search = new QueryBuilder<T>(dbName, index, query)
|
||||||
|
if (params.version) {
|
||||||
|
search.setVersion(params.version)
|
||||||
|
}
|
||||||
|
if (params.tableId) {
|
||||||
|
search.setTable(params.tableId)
|
||||||
|
}
|
||||||
|
if (params.sort) {
|
||||||
|
search
|
||||||
|
.setSort(params.sort)
|
||||||
|
.setSortOrder(params.sortOrder)
|
||||||
|
.setSortType(params.sortType)
|
||||||
|
}
|
||||||
|
if (params.indexer) {
|
||||||
|
search.setIndexBuilder(params.indexer)
|
||||||
|
}
|
||||||
|
if (params.disableEscaping) {
|
||||||
|
search.disableEscaping()
|
||||||
|
}
|
||||||
|
const searchResults = await search
|
||||||
|
.setBookmark(params.bookmark)
|
||||||
|
.setLimit(limit)
|
||||||
|
.run()
|
||||||
|
|
||||||
|
// Try fetching 1 row in the next page to see if another page of results
|
||||||
|
// exists or not
|
||||||
|
search.setBookmark(searchResults.bookmark).setLimit(1)
|
||||||
|
if (params.tableId) {
|
||||||
|
search.setTable(params.tableId)
|
||||||
|
}
|
||||||
|
const nextResults = await search.run()
|
||||||
|
|
||||||
|
return {
|
||||||
|
...searchResults,
|
||||||
|
hasNextPage: nextResults.rows && nextResults.rows.length > 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs a full search, fetching multiple pages if required to return the
|
||||||
|
* desired amount of results. There is a limit of 1000 results to avoid
|
||||||
|
* heavy performance hits, and to avoid client components breaking from
|
||||||
|
* handling too much data.
|
||||||
|
* @param dbName {string} Which database to run a lucene query on
|
||||||
|
* @param index {string} Which search index to utilise
|
||||||
|
* @param query {object} The JSON query structure
|
||||||
|
* @param params {object} The search params including:
|
||||||
|
* tableId {string} The table ID to search
|
||||||
|
* sort {string} The sort column
|
||||||
|
* sortOrder {string} The sort order ("ascending" or "descending")
|
||||||
|
* sortType {string} Whether to treat sortable values as strings or
|
||||||
|
* numbers. ("string" or "number")
|
||||||
|
* limit {number} The desired number of results
|
||||||
|
* @returns {Promise<{rows: *}>}
|
||||||
|
*/
|
||||||
|
export async function fullSearch<T>(
|
||||||
|
dbName: string,
|
||||||
|
index: string,
|
||||||
|
query: SearchFilters,
|
||||||
|
params: SearchParams<T>
|
||||||
|
) {
|
||||||
|
let limit = params.limit
|
||||||
|
if (limit == null || isNaN(limit) || limit < 0) {
|
||||||
|
limit = 1000
|
||||||
|
}
|
||||||
|
params.limit = Math.min(limit, 1000)
|
||||||
|
const rows = await recursiveSearch<T>(dbName, index, query, params)
|
||||||
|
return { rows }
|
||||||
|
}
|
|
@ -0,0 +1,161 @@
|
||||||
|
import { newid } from "../../newid"
|
||||||
|
import { getDB } from "../db"
|
||||||
|
import { Database } from "@budibase/types"
|
||||||
|
import { QueryBuilder, paginatedSearch, fullSearch } from "../lucene"
|
||||||
|
|
||||||
|
const INDEX_NAME = "main"
|
||||||
|
|
||||||
|
const index = `function(doc) {
|
||||||
|
let props = ["property", "number"]
|
||||||
|
for (let key of props) {
|
||||||
|
if (doc[key]) {
|
||||||
|
index(key, doc[key])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
describe("lucene", () => {
|
||||||
|
let db: Database, dbName: string
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
dbName = `db-${newid()}`
|
||||||
|
// create the DB for testing
|
||||||
|
db = getDB(dbName)
|
||||||
|
await db.put({ _id: newid(), property: "word" })
|
||||||
|
await db.put({ _id: newid(), property: "word2" })
|
||||||
|
await db.put({ _id: newid(), property: "word3", number: 1 })
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should be able to create a lucene index", async () => {
|
||||||
|
const response = await db.put({
|
||||||
|
_id: "_design/database",
|
||||||
|
indexes: {
|
||||||
|
[INDEX_NAME]: {
|
||||||
|
index: index,
|
||||||
|
analyzer: "standard",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
expect(response.ok).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("query builder", () => {
|
||||||
|
it("should be able to perform a basic query", async () => {
|
||||||
|
const builder = new QueryBuilder(dbName, INDEX_NAME)
|
||||||
|
builder.setSort("property")
|
||||||
|
builder.setSortOrder("desc")
|
||||||
|
builder.setSortType("string")
|
||||||
|
const resp = await builder.run()
|
||||||
|
expect(resp.rows.length).toBe(3)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle limits", async () => {
|
||||||
|
const builder = new QueryBuilder(dbName, INDEX_NAME)
|
||||||
|
builder.setLimit(1)
|
||||||
|
const resp = await builder.run()
|
||||||
|
expect(resp.rows.length).toBe(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should be able to perform a string search", async () => {
|
||||||
|
const builder = new QueryBuilder(dbName, INDEX_NAME)
|
||||||
|
builder.addString("property", "wo")
|
||||||
|
const resp = await builder.run()
|
||||||
|
expect(resp.rows.length).toBe(3)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should be able to perform a range search", async () => {
|
||||||
|
const builder = new QueryBuilder(dbName, INDEX_NAME)
|
||||||
|
builder.addRange("number", 0, 1)
|
||||||
|
const resp = await builder.run()
|
||||||
|
expect(resp.rows.length).toBe(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should be able to perform an equal search", async () => {
|
||||||
|
const builder = new QueryBuilder(dbName, INDEX_NAME)
|
||||||
|
builder.addEqual("property", "word2")
|
||||||
|
const resp = await builder.run()
|
||||||
|
expect(resp.rows.length).toBe(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should be able to perform a not equal search", async () => {
|
||||||
|
const builder = new QueryBuilder(dbName, INDEX_NAME)
|
||||||
|
builder.addNotEqual("property", "word2")
|
||||||
|
const resp = await builder.run()
|
||||||
|
expect(resp.rows.length).toBe(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should be able to perform an empty search", async () => {
|
||||||
|
const builder = new QueryBuilder(dbName, INDEX_NAME)
|
||||||
|
builder.addEmpty("number", true)
|
||||||
|
const resp = await builder.run()
|
||||||
|
expect(resp.rows.length).toBe(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should be able to perform a not empty search", async () => {
|
||||||
|
const builder = new QueryBuilder(dbName, INDEX_NAME)
|
||||||
|
builder.addNotEmpty("number", true)
|
||||||
|
const resp = await builder.run()
|
||||||
|
expect(resp.rows.length).toBe(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should be able to perform a one of search", async () => {
|
||||||
|
const builder = new QueryBuilder(dbName, INDEX_NAME)
|
||||||
|
builder.addOneOf("property", ["word", "word2"])
|
||||||
|
const resp = await builder.run()
|
||||||
|
expect(resp.rows.length).toBe(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should be able to perform a contains search", async () => {
|
||||||
|
const builder = new QueryBuilder(dbName, INDEX_NAME)
|
||||||
|
builder.addContains("property", ["word"])
|
||||||
|
const resp = await builder.run()
|
||||||
|
expect(resp.rows.length).toBe(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should be able to perform a not contains search", async () => {
|
||||||
|
const builder = new QueryBuilder(dbName, INDEX_NAME)
|
||||||
|
builder.addNotContains("property", ["word2"])
|
||||||
|
const resp = await builder.run()
|
||||||
|
expect(resp.rows.length).toBe(2)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("paginated search", () => {
|
||||||
|
it("should be able to perform a paginated search", async () => {
|
||||||
|
const page = await paginatedSearch(
|
||||||
|
dbName,
|
||||||
|
INDEX_NAME,
|
||||||
|
{
|
||||||
|
string: {
|
||||||
|
property: "wo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
limit: 1,
|
||||||
|
sort: "property",
|
||||||
|
sortType: "string",
|
||||||
|
sortOrder: "desc",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
expect(page.rows.length).toBe(1)
|
||||||
|
expect(page.hasNextPage).toBe(true)
|
||||||
|
expect(page.bookmark).toBeDefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("full search", () => {
|
||||||
|
it("should be able to perform a full search", async () => {
|
||||||
|
const page = await fullSearch(
|
||||||
|
dbName,
|
||||||
|
INDEX_NAME,
|
||||||
|
{
|
||||||
|
string: {
|
||||||
|
property: "wo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
expect(page.rows.length).toBe(3)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -365,6 +365,16 @@ export async function getAllApps({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getAppsByIDs(appIds: string[]) {
|
||||||
|
const settled = await Promise.allSettled(
|
||||||
|
appIds.map(appId => getAppMetadata(appId))
|
||||||
|
)
|
||||||
|
// have to list the apps which exist, some may have been deleted
|
||||||
|
return settled
|
||||||
|
.filter(promise => promise.status === "fulfilled")
|
||||||
|
.map(promise => (promise as PromiseFulfilledResult<App>).value)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility function for getAllApps but filters to production apps only.
|
* Utility function for getAllApps but filters to production apps only.
|
||||||
*/
|
*/
|
||||||
|
@ -381,6 +391,16 @@ export async function getDevAppIDs() {
|
||||||
return apps.filter((id: any) => isDevAppID(id))
|
return apps.filter((id: any) => isDevAppID(id))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isSameAppID(
|
||||||
|
appId1: string | undefined,
|
||||||
|
appId2: string | undefined
|
||||||
|
) {
|
||||||
|
if (appId1 == undefined || appId2 == undefined) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return getProdAppID(appId1) === getProdAppID(appId2)
|
||||||
|
}
|
||||||
|
|
||||||
export async function dbExists(dbName: any) {
|
export async function dbExists(dbName: any) {
|
||||||
return doWithDB(
|
return doWithDB(
|
||||||
dbName,
|
dbName,
|
||||||
|
|
|
@ -86,6 +86,7 @@ const environment = {
|
||||||
DEPLOYMENT_ENVIRONMENT:
|
DEPLOYMENT_ENVIRONMENT:
|
||||||
process.env.DEPLOYMENT_ENVIRONMENT || "docker-compose",
|
process.env.DEPLOYMENT_ENVIRONMENT || "docker-compose",
|
||||||
ENABLE_4XX_HTTP_LOGGING: process.env.ENABLE_4XX_HTTP_LOGGING || true,
|
ENABLE_4XX_HTTP_LOGGING: process.env.ENABLE_4XX_HTTP_LOGGING || true,
|
||||||
|
ENABLE_AUDIT_LOG_IP_ADDR: process.env.ENABLE_AUDIT_LOG_IP_ADDR,
|
||||||
// smtp
|
// smtp
|
||||||
SMTP_FALLBACK_ENABLED: process.env.SMTP_FALLBACK_ENABLED,
|
SMTP_FALLBACK_ENABLED: process.env.SMTP_FALLBACK_ENABLED,
|
||||||
SMTP_USER: process.env.SMTP_USER,
|
SMTP_USER: process.env.SMTP_USER,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Event } from "@budibase/types"
|
import { Event, AuditedEventFriendlyName } from "@budibase/types"
|
||||||
import { processors } from "./processors"
|
import { processors } from "./processors"
|
||||||
import identification from "./identification"
|
import identification from "./identification"
|
||||||
import * as backfill from "./backfill"
|
import * as backfill from "./backfill"
|
||||||
|
|
|
@ -87,6 +87,7 @@ const getCurrentIdentity = async (): Promise<Identity> => {
|
||||||
installationId,
|
installationId,
|
||||||
tenantId,
|
tenantId,
|
||||||
environment,
|
environment,
|
||||||
|
hostInfo: userContext.hostInfo,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error("Unknown identity type")
|
throw new Error("Unknown identity type")
|
||||||
|
|
|
@ -0,0 +1,90 @@
|
||||||
|
import {
|
||||||
|
Event,
|
||||||
|
Identity,
|
||||||
|
Group,
|
||||||
|
IdentityType,
|
||||||
|
AuditLogQueueEvent,
|
||||||
|
AuditLogFn,
|
||||||
|
HostInfo,
|
||||||
|
} from "@budibase/types"
|
||||||
|
import { EventProcessor } from "./types"
|
||||||
|
import { getAppId } from "../../context"
|
||||||
|
import BullQueue from "bull"
|
||||||
|
import { createQueue, JobQueue } from "../../queue"
|
||||||
|
import { isAudited } from "../../utils"
|
||||||
|
import env from "../../environment"
|
||||||
|
|
||||||
|
export default class AuditLogsProcessor implements EventProcessor {
|
||||||
|
static auditLogsEnabled = false
|
||||||
|
static auditLogQueue: BullQueue.Queue<AuditLogQueueEvent>
|
||||||
|
|
||||||
|
// can't use constructor as need to return promise
|
||||||
|
static init(fn: AuditLogFn) {
|
||||||
|
AuditLogsProcessor.auditLogsEnabled = true
|
||||||
|
const writeAuditLogs = fn
|
||||||
|
AuditLogsProcessor.auditLogQueue = createQueue<AuditLogQueueEvent>(
|
||||||
|
JobQueue.AUDIT_LOG
|
||||||
|
)
|
||||||
|
return AuditLogsProcessor.auditLogQueue.process(async job => {
|
||||||
|
let properties = job.data.properties
|
||||||
|
if (properties.audited) {
|
||||||
|
properties = {
|
||||||
|
...properties,
|
||||||
|
...properties.audited,
|
||||||
|
}
|
||||||
|
delete properties.audited
|
||||||
|
}
|
||||||
|
|
||||||
|
// this feature is disabled by default due to privacy requirements
|
||||||
|
// in some countries - available as env var in-case it is desired
|
||||||
|
// in self host deployments
|
||||||
|
let hostInfo: HostInfo | undefined = {}
|
||||||
|
if (env.ENABLE_AUDIT_LOG_IP_ADDR) {
|
||||||
|
hostInfo = job.data.opts.hostInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
await writeAuditLogs(job.data.event, properties, {
|
||||||
|
userId: job.data.opts.userId,
|
||||||
|
timestamp: job.data.opts.timestamp,
|
||||||
|
appId: job.data.opts.appId,
|
||||||
|
hostInfo,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async processEvent(
|
||||||
|
event: Event,
|
||||||
|
identity: Identity,
|
||||||
|
properties: any,
|
||||||
|
timestamp?: string
|
||||||
|
): Promise<void> {
|
||||||
|
if (AuditLogsProcessor.auditLogsEnabled && isAudited(event)) {
|
||||||
|
// only audit log actual events, don't include backfills
|
||||||
|
const userId =
|
||||||
|
identity.type === IdentityType.USER ? identity.id : undefined
|
||||||
|
// add to the event queue, rather than just writing immediately
|
||||||
|
await AuditLogsProcessor.auditLogQueue.add({
|
||||||
|
event,
|
||||||
|
properties,
|
||||||
|
opts: {
|
||||||
|
userId,
|
||||||
|
timestamp,
|
||||||
|
appId: getAppId(),
|
||||||
|
hostInfo: identity.hostInfo,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async identify(identity: Identity, timestamp?: string | number) {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
|
||||||
|
async identifyGroup(group: Group, timestamp?: string | number) {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
|
||||||
|
shutdown(): void {
|
||||||
|
AuditLogsProcessor.auditLogQueue?.close()
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,19 @@
|
||||||
import AnalyticsProcessor from "./AnalyticsProcessor"
|
import AnalyticsProcessor from "./AnalyticsProcessor"
|
||||||
import LoggingProcessor from "./LoggingProcessor"
|
import LoggingProcessor from "./LoggingProcessor"
|
||||||
|
import AuditLogsProcessor from "./AuditLogsProcessor"
|
||||||
import Processors from "./Processors"
|
import Processors from "./Processors"
|
||||||
|
import { AuditLogFn } from "@budibase/types"
|
||||||
|
|
||||||
export const analyticsProcessor = new AnalyticsProcessor()
|
export const analyticsProcessor = new AnalyticsProcessor()
|
||||||
const loggingProcessor = new LoggingProcessor()
|
const loggingProcessor = new LoggingProcessor()
|
||||||
|
const auditLogsProcessor = new AuditLogsProcessor()
|
||||||
|
|
||||||
export const processors = new Processors([analyticsProcessor, loggingProcessor])
|
export function init(auditingFn: AuditLogFn) {
|
||||||
|
return AuditLogsProcessor.init(auditingFn)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const processors = new Processors([
|
||||||
|
analyticsProcessor,
|
||||||
|
loggingProcessor,
|
||||||
|
auditLogsProcessor,
|
||||||
|
])
|
||||||
|
|
|
@ -47,6 +47,8 @@ export default class PosthogProcessor implements EventProcessor {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
properties = this.clearPIIProperties(properties)
|
||||||
|
|
||||||
properties.version = pkg.version
|
properties.version = pkg.version
|
||||||
properties.service = env.SERVICE
|
properties.service = env.SERVICE
|
||||||
properties.environment = identity.environment
|
properties.environment = identity.environment
|
||||||
|
@ -79,6 +81,16 @@ export default class PosthogProcessor implements EventProcessor {
|
||||||
this.posthog.capture(payload)
|
this.posthog.capture(payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clearPIIProperties(properties: any) {
|
||||||
|
if (properties.email) {
|
||||||
|
delete properties.email
|
||||||
|
}
|
||||||
|
if (properties.audited) {
|
||||||
|
delete properties.audited
|
||||||
|
}
|
||||||
|
return properties
|
||||||
|
}
|
||||||
|
|
||||||
async identify(identity: Identity, timestamp?: string | number) {
|
async identify(identity: Identity, timestamp?: string | number) {
|
||||||
const payload: any = { distinctId: identity.id, properties: identity }
|
const payload: any = { distinctId: identity.id, properties: identity }
|
||||||
if (timestamp) {
|
if (timestamp) {
|
||||||
|
|
|
@ -49,6 +49,25 @@ describe("PosthogProcessor", () => {
|
||||||
expect(processor.posthog.capture).toHaveBeenCalledTimes(0)
|
expect(processor.posthog.capture).toHaveBeenCalledTimes(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("removes audited information", async () => {
|
||||||
|
const processor = new PosthogProcessor("test")
|
||||||
|
|
||||||
|
const identity = newIdentity()
|
||||||
|
const properties = {
|
||||||
|
email: "test",
|
||||||
|
audited: {
|
||||||
|
name: "test",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
await processor.processEvent(Event.USER_CREATED, identity, properties)
|
||||||
|
expect(processor.posthog.capture).toHaveBeenCalled()
|
||||||
|
// @ts-ignore
|
||||||
|
const call = processor.posthog.capture.mock.calls[0][0]
|
||||||
|
expect(call.properties.audited).toBeUndefined()
|
||||||
|
expect(call.properties.email).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
describe("rate limiting", () => {
|
describe("rate limiting", () => {
|
||||||
it("sends daily event once in same day", async () => {
|
it("sends daily event once in same day", async () => {
|
||||||
const processor = new PosthogProcessor("test")
|
const processor = new PosthogProcessor("test")
|
||||||
|
|
|
@ -19,6 +19,9 @@ const created = async (app: App, timestamp?: string | number) => {
|
||||||
const properties: AppCreatedEvent = {
|
const properties: AppCreatedEvent = {
|
||||||
appId: app.appId,
|
appId: app.appId,
|
||||||
version: app.version,
|
version: app.version,
|
||||||
|
audited: {
|
||||||
|
name: app.name,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
await publishEvent(Event.APP_CREATED, properties, timestamp)
|
await publishEvent(Event.APP_CREATED, properties, timestamp)
|
||||||
}
|
}
|
||||||
|
@ -27,6 +30,9 @@ async function updated(app: App) {
|
||||||
const properties: AppUpdatedEvent = {
|
const properties: AppUpdatedEvent = {
|
||||||
appId: app.appId,
|
appId: app.appId,
|
||||||
version: app.version,
|
version: app.version,
|
||||||
|
audited: {
|
||||||
|
name: app.name,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
await publishEvent(Event.APP_UPDATED, properties)
|
await publishEvent(Event.APP_UPDATED, properties)
|
||||||
}
|
}
|
||||||
|
@ -34,6 +40,9 @@ async function updated(app: App) {
|
||||||
async function deleted(app: App) {
|
async function deleted(app: App) {
|
||||||
const properties: AppDeletedEvent = {
|
const properties: AppDeletedEvent = {
|
||||||
appId: app.appId,
|
appId: app.appId,
|
||||||
|
audited: {
|
||||||
|
name: app.name,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
await publishEvent(Event.APP_DELETED, properties)
|
await publishEvent(Event.APP_DELETED, properties)
|
||||||
}
|
}
|
||||||
|
@ -41,6 +50,9 @@ async function deleted(app: App) {
|
||||||
async function published(app: App, timestamp?: string | number) {
|
async function published(app: App, timestamp?: string | number) {
|
||||||
const properties: AppPublishedEvent = {
|
const properties: AppPublishedEvent = {
|
||||||
appId: app.appId,
|
appId: app.appId,
|
||||||
|
audited: {
|
||||||
|
name: app.name,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
await publishEvent(Event.APP_PUBLISHED, properties, timestamp)
|
await publishEvent(Event.APP_PUBLISHED, properties, timestamp)
|
||||||
}
|
}
|
||||||
|
@ -48,6 +60,9 @@ async function published(app: App, timestamp?: string | number) {
|
||||||
async function unpublished(app: App) {
|
async function unpublished(app: App) {
|
||||||
const properties: AppUnpublishedEvent = {
|
const properties: AppUnpublishedEvent = {
|
||||||
appId: app.appId,
|
appId: app.appId,
|
||||||
|
audited: {
|
||||||
|
name: app.name,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
await publishEvent(Event.APP_UNPUBLISHED, properties)
|
await publishEvent(Event.APP_UNPUBLISHED, properties)
|
||||||
}
|
}
|
||||||
|
@ -55,6 +70,9 @@ async function unpublished(app: App) {
|
||||||
async function fileImported(app: App) {
|
async function fileImported(app: App) {
|
||||||
const properties: AppFileImportedEvent = {
|
const properties: AppFileImportedEvent = {
|
||||||
appId: app.appId,
|
appId: app.appId,
|
||||||
|
audited: {
|
||||||
|
name: app.name,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
await publishEvent(Event.APP_FILE_IMPORTED, properties)
|
await publishEvent(Event.APP_FILE_IMPORTED, properties)
|
||||||
}
|
}
|
||||||
|
@ -63,6 +81,9 @@ async function templateImported(app: App, templateKey: string) {
|
||||||
const properties: AppTemplateImportedEvent = {
|
const properties: AppTemplateImportedEvent = {
|
||||||
appId: app.appId,
|
appId: app.appId,
|
||||||
templateKey,
|
templateKey,
|
||||||
|
audited: {
|
||||||
|
name: app.name,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
await publishEvent(Event.APP_TEMPLATE_IMPORTED, properties)
|
await publishEvent(Event.APP_TEMPLATE_IMPORTED, properties)
|
||||||
}
|
}
|
||||||
|
@ -76,6 +97,9 @@ async function versionUpdated(
|
||||||
appId: app.appId,
|
appId: app.appId,
|
||||||
currentVersion,
|
currentVersion,
|
||||||
updatedToVersion,
|
updatedToVersion,
|
||||||
|
audited: {
|
||||||
|
name: app.name,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
await publishEvent(Event.APP_VERSION_UPDATED, properties)
|
await publishEvent(Event.APP_VERSION_UPDATED, properties)
|
||||||
}
|
}
|
||||||
|
@ -89,6 +113,9 @@ async function versionReverted(
|
||||||
appId: app.appId,
|
appId: app.appId,
|
||||||
currentVersion,
|
currentVersion,
|
||||||
revertedToVersion,
|
revertedToVersion,
|
||||||
|
audited: {
|
||||||
|
name: app.name,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
await publishEvent(Event.APP_VERSION_REVERTED, properties)
|
await publishEvent(Event.APP_VERSION_REVERTED, properties)
|
||||||
}
|
}
|
||||||
|
@ -96,6 +123,9 @@ async function versionReverted(
|
||||||
async function reverted(app: App) {
|
async function reverted(app: App) {
|
||||||
const properties: AppRevertedEvent = {
|
const properties: AppRevertedEvent = {
|
||||||
appId: app.appId,
|
appId: app.appId,
|
||||||
|
audited: {
|
||||||
|
name: app.name,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
await publishEvent(Event.APP_REVERTED, properties)
|
await publishEvent(Event.APP_REVERTED, properties)
|
||||||
}
|
}
|
||||||
|
@ -103,6 +133,9 @@ async function reverted(app: App) {
|
||||||
async function exported(app: App) {
|
async function exported(app: App) {
|
||||||
const properties: AppExportedEvent = {
|
const properties: AppExportedEvent = {
|
||||||
appId: app.appId,
|
appId: app.appId,
|
||||||
|
audited: {
|
||||||
|
name: app.name,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
await publishEvent(Event.APP_EXPORTED, properties)
|
await publishEvent(Event.APP_EXPORTED, properties)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
import {
|
||||||
|
Event,
|
||||||
|
AuditLogSearchParams,
|
||||||
|
AuditLogFilteredEvent,
|
||||||
|
AuditLogDownloadedEvent,
|
||||||
|
} from "@budibase/types"
|
||||||
|
import { publishEvent } from "../events"
|
||||||
|
|
||||||
|
async function filtered(search: AuditLogSearchParams) {
|
||||||
|
const properties: AuditLogFilteredEvent = {
|
||||||
|
filters: search,
|
||||||
|
}
|
||||||
|
await publishEvent(Event.AUDIT_LOGS_FILTERED, properties)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloaded(search: AuditLogSearchParams) {
|
||||||
|
const properties: AuditLogDownloadedEvent = {
|
||||||
|
filters: search,
|
||||||
|
}
|
||||||
|
await publishEvent(Event.AUDIT_LOGS_DOWNLOADED, properties)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
filtered,
|
||||||
|
downloaded,
|
||||||
|
}
|
|
@ -12,19 +12,25 @@ import {
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { identification } from ".."
|
import { identification } from ".."
|
||||||
|
|
||||||
async function login(source: LoginSource) {
|
async function login(source: LoginSource, email: string) {
|
||||||
const identity = await identification.getCurrentIdentity()
|
const identity = await identification.getCurrentIdentity()
|
||||||
const properties: LoginEvent = {
|
const properties: LoginEvent = {
|
||||||
userId: identity.id,
|
userId: identity.id,
|
||||||
source,
|
source,
|
||||||
|
audited: {
|
||||||
|
email,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
await publishEvent(Event.AUTH_LOGIN, properties)
|
await publishEvent(Event.AUTH_LOGIN, properties)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function logout() {
|
async function logout(email?: string) {
|
||||||
const identity = await identification.getCurrentIdentity()
|
const identity = await identification.getCurrentIdentity()
|
||||||
const properties: LogoutEvent = {
|
const properties: LogoutEvent = {
|
||||||
userId: identity.id,
|
userId: identity.id,
|
||||||
|
audited: {
|
||||||
|
email,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
await publishEvent(Event.AUTH_LOGOUT, properties)
|
await publishEvent(Event.AUTH_LOGOUT, properties)
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,9 @@ async function created(automation: Automation, timestamp?: string | number) {
|
||||||
automationId: automation._id as string,
|
automationId: automation._id as string,
|
||||||
triggerId: automation.definition?.trigger?.id,
|
triggerId: automation.definition?.trigger?.id,
|
||||||
triggerType: automation.definition?.trigger?.stepId,
|
triggerType: automation.definition?.trigger?.stepId,
|
||||||
|
audited: {
|
||||||
|
name: automation.name,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
await publishEvent(Event.AUTOMATION_CREATED, properties, timestamp)
|
await publishEvent(Event.AUTOMATION_CREATED, properties, timestamp)
|
||||||
}
|
}
|
||||||
|
@ -38,6 +41,9 @@ async function deleted(automation: Automation) {
|
||||||
automationId: automation._id as string,
|
automationId: automation._id as string,
|
||||||
triggerId: automation.definition?.trigger?.id,
|
triggerId: automation.definition?.trigger?.id,
|
||||||
triggerType: automation.definition?.trigger?.stepId,
|
triggerType: automation.definition?.trigger?.stepId,
|
||||||
|
audited: {
|
||||||
|
name: automation.name,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
await publishEvent(Event.AUTOMATION_DELETED, properties)
|
await publishEvent(Event.AUTOMATION_DELETED, properties)
|
||||||
}
|
}
|
||||||
|
@ -71,6 +77,9 @@ async function stepCreated(
|
||||||
triggerType: automation.definition?.trigger?.stepId,
|
triggerType: automation.definition?.trigger?.stepId,
|
||||||
stepId: step.id!,
|
stepId: step.id!,
|
||||||
stepType: step.stepId,
|
stepType: step.stepId,
|
||||||
|
audited: {
|
||||||
|
name: automation.name,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
await publishEvent(Event.AUTOMATION_STEP_CREATED, properties, timestamp)
|
await publishEvent(Event.AUTOMATION_STEP_CREATED, properties, timestamp)
|
||||||
}
|
}
|
||||||
|
@ -83,6 +92,9 @@ async function stepDeleted(automation: Automation, step: AutomationStep) {
|
||||||
triggerType: automation.definition?.trigger?.stepId,
|
triggerType: automation.definition?.trigger?.stepId,
|
||||||
stepId: step.id!,
|
stepId: step.id!,
|
||||||
stepType: step.stepId,
|
stepType: step.stepId,
|
||||||
|
audited: {
|
||||||
|
name: automation.name,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
await publishEvent(Event.AUTOMATION_STEP_DELETED, properties)
|
await publishEvent(Event.AUTOMATION_STEP_DELETED, properties)
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ async function appBackupRestored(backup: AppBackup) {
|
||||||
appId: backup.appId,
|
appId: backup.appId,
|
||||||
restoreId: backup._id!,
|
restoreId: backup._id!,
|
||||||
backupCreatedAt: backup.timestamp,
|
backupCreatedAt: backup.timestamp,
|
||||||
|
name: backup.name as string,
|
||||||
}
|
}
|
||||||
|
|
||||||
await publishEvent(Event.APP_BACKUP_RESTORED, properties)
|
await publishEvent(Event.APP_BACKUP_RESTORED, properties)
|
||||||
|
@ -22,13 +23,15 @@ async function appBackupTriggered(
|
||||||
appId: string,
|
appId: string,
|
||||||
backupId: string,
|
backupId: string,
|
||||||
type: AppBackupType,
|
type: AppBackupType,
|
||||||
trigger: AppBackupTrigger
|
trigger: AppBackupTrigger,
|
||||||
|
name: string
|
||||||
) {
|
) {
|
||||||
const properties: AppBackupTriggeredEvent = {
|
const properties: AppBackupTriggeredEvent = {
|
||||||
appId: appId,
|
appId: appId,
|
||||||
backupId,
|
backupId,
|
||||||
type,
|
type,
|
||||||
trigger,
|
trigger,
|
||||||
|
name,
|
||||||
}
|
}
|
||||||
await publishEvent(Event.APP_BACKUP_TRIGGERED, properties)
|
await publishEvent(Event.APP_BACKUP_TRIGGERED, properties)
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,12 +8,16 @@ import {
|
||||||
GroupUsersAddedEvent,
|
GroupUsersAddedEvent,
|
||||||
GroupUsersDeletedEvent,
|
GroupUsersDeletedEvent,
|
||||||
GroupAddedOnboardingEvent,
|
GroupAddedOnboardingEvent,
|
||||||
|
GroupPermissionsEditedEvent,
|
||||||
UserGroupRoles,
|
UserGroupRoles,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
|
|
||||||
async function created(group: UserGroup, timestamp?: number) {
|
async function created(group: UserGroup, timestamp?: number) {
|
||||||
const properties: GroupCreatedEvent = {
|
const properties: GroupCreatedEvent = {
|
||||||
groupId: group._id as string,
|
groupId: group._id as string,
|
||||||
|
audited: {
|
||||||
|
name: group.name,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
await publishEvent(Event.USER_GROUP_CREATED, properties, timestamp)
|
await publishEvent(Event.USER_GROUP_CREATED, properties, timestamp)
|
||||||
}
|
}
|
||||||
|
@ -21,6 +25,9 @@ async function created(group: UserGroup, timestamp?: number) {
|
||||||
async function updated(group: UserGroup) {
|
async function updated(group: UserGroup) {
|
||||||
const properties: GroupUpdatedEvent = {
|
const properties: GroupUpdatedEvent = {
|
||||||
groupId: group._id as string,
|
groupId: group._id as string,
|
||||||
|
audited: {
|
||||||
|
name: group.name,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
await publishEvent(Event.USER_GROUP_UPDATED, properties)
|
await publishEvent(Event.USER_GROUP_UPDATED, properties)
|
||||||
}
|
}
|
||||||
|
@ -28,6 +35,9 @@ async function updated(group: UserGroup) {
|
||||||
async function deleted(group: UserGroup) {
|
async function deleted(group: UserGroup) {
|
||||||
const properties: GroupDeletedEvent = {
|
const properties: GroupDeletedEvent = {
|
||||||
groupId: group._id as string,
|
groupId: group._id as string,
|
||||||
|
audited: {
|
||||||
|
name: group.name,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
await publishEvent(Event.USER_GROUP_DELETED, properties)
|
await publishEvent(Event.USER_GROUP_DELETED, properties)
|
||||||
}
|
}
|
||||||
|
@ -36,6 +46,9 @@ async function usersAdded(count: number, group: UserGroup) {
|
||||||
const properties: GroupUsersAddedEvent = {
|
const properties: GroupUsersAddedEvent = {
|
||||||
count,
|
count,
|
||||||
groupId: group._id as string,
|
groupId: group._id as string,
|
||||||
|
audited: {
|
||||||
|
name: group.name,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
await publishEvent(Event.USER_GROUP_USERS_ADDED, properties)
|
await publishEvent(Event.USER_GROUP_USERS_ADDED, properties)
|
||||||
}
|
}
|
||||||
|
@ -44,6 +57,9 @@ async function usersDeleted(count: number, group: UserGroup) {
|
||||||
const properties: GroupUsersDeletedEvent = {
|
const properties: GroupUsersDeletedEvent = {
|
||||||
count,
|
count,
|
||||||
groupId: group._id as string,
|
groupId: group._id as string,
|
||||||
|
audited: {
|
||||||
|
name: group.name,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
await publishEvent(Event.USER_GROUP_USERS_REMOVED, properties)
|
await publishEvent(Event.USER_GROUP_USERS_REMOVED, properties)
|
||||||
}
|
}
|
||||||
|
@ -56,9 +72,13 @@ async function createdOnboarding(groupId: string) {
|
||||||
await publishEvent(Event.USER_GROUP_ONBOARDING, properties)
|
await publishEvent(Event.USER_GROUP_ONBOARDING, properties)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function permissionsEdited(roles: UserGroupRoles) {
|
async function permissionsEdited(group: UserGroup) {
|
||||||
const properties: UserGroupRoles = {
|
const properties: GroupPermissionsEditedEvent = {
|
||||||
...roles,
|
permissions: group.roles!,
|
||||||
|
groupId: group._id as string,
|
||||||
|
audited: {
|
||||||
|
name: group.name,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
await publishEvent(Event.USER_GROUP_PERMISSIONS_EDITED, properties)
|
await publishEvent(Event.USER_GROUP_PERMISSIONS_EDITED, properties)
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,3 +21,4 @@ export { default as group } from "./group"
|
||||||
export { default as plugin } from "./plugin"
|
export { default as plugin } from "./plugin"
|
||||||
export { default as backup } from "./backup"
|
export { default as backup } from "./backup"
|
||||||
export { default as environmentVariable } from "./environmentVariable"
|
export { default as environmentVariable } from "./environmentVariable"
|
||||||
|
export { default as auditLog } from "./auditLog"
|
||||||
|
|
|
@ -11,6 +11,9 @@ async function created(screen: Screen, timestamp?: string | number) {
|
||||||
layoutId: screen.layoutId,
|
layoutId: screen.layoutId,
|
||||||
screenId: screen._id as string,
|
screenId: screen._id as string,
|
||||||
roleId: screen.routing.roleId,
|
roleId: screen.routing.roleId,
|
||||||
|
audited: {
|
||||||
|
name: screen.routing?.route,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
await publishEvent(Event.SCREEN_CREATED, properties, timestamp)
|
await publishEvent(Event.SCREEN_CREATED, properties, timestamp)
|
||||||
}
|
}
|
||||||
|
@ -20,6 +23,9 @@ async function deleted(screen: Screen) {
|
||||||
layoutId: screen.layoutId,
|
layoutId: screen.layoutId,
|
||||||
screenId: screen._id as string,
|
screenId: screen._id as string,
|
||||||
roleId: screen.routing.roleId,
|
roleId: screen.routing.roleId,
|
||||||
|
audited: {
|
||||||
|
name: screen.routing?.route,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
await publishEvent(Event.SCREEN_DELETED, properties)
|
await publishEvent(Event.SCREEN_DELETED, properties)
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,9 @@ import {
|
||||||
async function created(table: Table, timestamp?: string | number) {
|
async function created(table: Table, timestamp?: string | number) {
|
||||||
const properties: TableCreatedEvent = {
|
const properties: TableCreatedEvent = {
|
||||||
tableId: table._id as string,
|
tableId: table._id as string,
|
||||||
|
audited: {
|
||||||
|
name: table.name,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
await publishEvent(Event.TABLE_CREATED, properties, timestamp)
|
await publishEvent(Event.TABLE_CREATED, properties, timestamp)
|
||||||
}
|
}
|
||||||
|
@ -20,6 +23,9 @@ async function created(table: Table, timestamp?: string | number) {
|
||||||
async function updated(table: Table) {
|
async function updated(table: Table) {
|
||||||
const properties: TableUpdatedEvent = {
|
const properties: TableUpdatedEvent = {
|
||||||
tableId: table._id as string,
|
tableId: table._id as string,
|
||||||
|
audited: {
|
||||||
|
name: table.name,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
await publishEvent(Event.TABLE_UPDATED, properties)
|
await publishEvent(Event.TABLE_UPDATED, properties)
|
||||||
}
|
}
|
||||||
|
@ -27,6 +33,9 @@ async function updated(table: Table) {
|
||||||
async function deleted(table: Table) {
|
async function deleted(table: Table) {
|
||||||
const properties: TableDeletedEvent = {
|
const properties: TableDeletedEvent = {
|
||||||
tableId: table._id as string,
|
tableId: table._id as string,
|
||||||
|
audited: {
|
||||||
|
name: table.name,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
await publishEvent(Event.TABLE_DELETED, properties)
|
await publishEvent(Event.TABLE_DELETED, properties)
|
||||||
}
|
}
|
||||||
|
@ -35,6 +44,9 @@ async function exported(table: Table, format: TableExportFormat) {
|
||||||
const properties: TableExportedEvent = {
|
const properties: TableExportedEvent = {
|
||||||
tableId: table._id as string,
|
tableId: table._id as string,
|
||||||
format,
|
format,
|
||||||
|
audited: {
|
||||||
|
name: table.name,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
await publishEvent(Event.TABLE_EXPORTED, properties)
|
await publishEvent(Event.TABLE_EXPORTED, properties)
|
||||||
}
|
}
|
||||||
|
@ -42,6 +54,9 @@ async function exported(table: Table, format: TableExportFormat) {
|
||||||
async function imported(table: Table) {
|
async function imported(table: Table) {
|
||||||
const properties: TableImportedEvent = {
|
const properties: TableImportedEvent = {
|
||||||
tableId: table._id as string,
|
tableId: table._id as string,
|
||||||
|
audited: {
|
||||||
|
name: table.name,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
await publishEvent(Event.TABLE_IMPORTED, properties)
|
await publishEvent(Event.TABLE_IMPORTED, properties)
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,9 @@ import {
|
||||||
async function created(user: User, timestamp?: number) {
|
async function created(user: User, timestamp?: number) {
|
||||||
const properties: UserCreatedEvent = {
|
const properties: UserCreatedEvent = {
|
||||||
userId: user._id as string,
|
userId: user._id as string,
|
||||||
|
audited: {
|
||||||
|
email: user.email,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
await publishEvent(Event.USER_CREATED, properties, timestamp)
|
await publishEvent(Event.USER_CREATED, properties, timestamp)
|
||||||
}
|
}
|
||||||
|
@ -26,6 +29,9 @@ async function created(user: User, timestamp?: number) {
|
||||||
async function updated(user: User) {
|
async function updated(user: User) {
|
||||||
const properties: UserUpdatedEvent = {
|
const properties: UserUpdatedEvent = {
|
||||||
userId: user._id as string,
|
userId: user._id as string,
|
||||||
|
audited: {
|
||||||
|
email: user.email,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
await publishEvent(Event.USER_UPDATED, properties)
|
await publishEvent(Event.USER_UPDATED, properties)
|
||||||
}
|
}
|
||||||
|
@ -33,6 +39,9 @@ async function updated(user: User) {
|
||||||
async function deleted(user: User) {
|
async function deleted(user: User) {
|
||||||
const properties: UserDeletedEvent = {
|
const properties: UserDeletedEvent = {
|
||||||
userId: user._id as string,
|
userId: user._id as string,
|
||||||
|
audited: {
|
||||||
|
email: user.email,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
await publishEvent(Event.USER_DELETED, properties)
|
await publishEvent(Event.USER_DELETED, properties)
|
||||||
}
|
}
|
||||||
|
@ -40,6 +49,9 @@ async function deleted(user: User) {
|
||||||
export async function onboardingComplete(user: User) {
|
export async function onboardingComplete(user: User) {
|
||||||
const properties: UserOnboardingEvent = {
|
const properties: UserOnboardingEvent = {
|
||||||
userId: user._id as string,
|
userId: user._id as string,
|
||||||
|
audited: {
|
||||||
|
email: user.email,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
await publishEvent(Event.USER_ONBOARDING_COMPLETE, properties)
|
await publishEvent(Event.USER_ONBOARDING_COMPLETE, properties)
|
||||||
}
|
}
|
||||||
|
@ -49,6 +61,9 @@ export async function onboardingComplete(user: User) {
|
||||||
async function permissionAdminAssigned(user: User, timestamp?: number) {
|
async function permissionAdminAssigned(user: User, timestamp?: number) {
|
||||||
const properties: UserPermissionAssignedEvent = {
|
const properties: UserPermissionAssignedEvent = {
|
||||||
userId: user._id as string,
|
userId: user._id as string,
|
||||||
|
audited: {
|
||||||
|
email: user.email,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
await publishEvent(
|
await publishEvent(
|
||||||
Event.USER_PERMISSION_ADMIN_ASSIGNED,
|
Event.USER_PERMISSION_ADMIN_ASSIGNED,
|
||||||
|
@ -60,6 +75,9 @@ async function permissionAdminAssigned(user: User, timestamp?: number) {
|
||||||
async function permissionAdminRemoved(user: User) {
|
async function permissionAdminRemoved(user: User) {
|
||||||
const properties: UserPermissionRemovedEvent = {
|
const properties: UserPermissionRemovedEvent = {
|
||||||
userId: user._id as string,
|
userId: user._id as string,
|
||||||
|
audited: {
|
||||||
|
email: user.email,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
await publishEvent(Event.USER_PERMISSION_ADMIN_REMOVED, properties)
|
await publishEvent(Event.USER_PERMISSION_ADMIN_REMOVED, properties)
|
||||||
}
|
}
|
||||||
|
@ -67,6 +85,9 @@ async function permissionAdminRemoved(user: User) {
|
||||||
async function permissionBuilderAssigned(user: User, timestamp?: number) {
|
async function permissionBuilderAssigned(user: User, timestamp?: number) {
|
||||||
const properties: UserPermissionAssignedEvent = {
|
const properties: UserPermissionAssignedEvent = {
|
||||||
userId: user._id as string,
|
userId: user._id as string,
|
||||||
|
audited: {
|
||||||
|
email: user.email,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
await publishEvent(
|
await publishEvent(
|
||||||
Event.USER_PERMISSION_BUILDER_ASSIGNED,
|
Event.USER_PERMISSION_BUILDER_ASSIGNED,
|
||||||
|
@ -78,20 +99,30 @@ async function permissionBuilderAssigned(user: User, timestamp?: number) {
|
||||||
async function permissionBuilderRemoved(user: User) {
|
async function permissionBuilderRemoved(user: User) {
|
||||||
const properties: UserPermissionRemovedEvent = {
|
const properties: UserPermissionRemovedEvent = {
|
||||||
userId: user._id as string,
|
userId: user._id as string,
|
||||||
|
audited: {
|
||||||
|
email: user.email,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
await publishEvent(Event.USER_PERMISSION_BUILDER_REMOVED, properties)
|
await publishEvent(Event.USER_PERMISSION_BUILDER_REMOVED, properties)
|
||||||
}
|
}
|
||||||
|
|
||||||
// INVITE
|
// INVITE
|
||||||
|
|
||||||
async function invited() {
|
async function invited(email: string) {
|
||||||
const properties: UserInvitedEvent = {}
|
const properties: UserInvitedEvent = {
|
||||||
|
audited: {
|
||||||
|
email,
|
||||||
|
},
|
||||||
|
}
|
||||||
await publishEvent(Event.USER_INVITED, properties)
|
await publishEvent(Event.USER_INVITED, properties)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function inviteAccepted(user: User) {
|
async function inviteAccepted(user: User) {
|
||||||
const properties: UserInviteAcceptedEvent = {
|
const properties: UserInviteAcceptedEvent = {
|
||||||
userId: user._id as string,
|
userId: user._id as string,
|
||||||
|
audited: {
|
||||||
|
email: user.email,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
await publishEvent(Event.USER_INVITED_ACCEPTED, properties)
|
await publishEvent(Event.USER_INVITED_ACCEPTED, properties)
|
||||||
}
|
}
|
||||||
|
@ -101,6 +132,9 @@ async function inviteAccepted(user: User) {
|
||||||
async function passwordForceReset(user: User) {
|
async function passwordForceReset(user: User) {
|
||||||
const properties: UserPasswordForceResetEvent = {
|
const properties: UserPasswordForceResetEvent = {
|
||||||
userId: user._id as string,
|
userId: user._id as string,
|
||||||
|
audited: {
|
||||||
|
email: user.email,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
await publishEvent(Event.USER_PASSWORD_FORCE_RESET, properties)
|
await publishEvent(Event.USER_PASSWORD_FORCE_RESET, properties)
|
||||||
}
|
}
|
||||||
|
@ -108,6 +142,9 @@ async function passwordForceReset(user: User) {
|
||||||
async function passwordUpdated(user: User) {
|
async function passwordUpdated(user: User) {
|
||||||
const properties: UserPasswordUpdatedEvent = {
|
const properties: UserPasswordUpdatedEvent = {
|
||||||
userId: user._id as string,
|
userId: user._id as string,
|
||||||
|
audited: {
|
||||||
|
email: user.email,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
await publishEvent(Event.USER_PASSWORD_UPDATED, properties)
|
await publishEvent(Event.USER_PASSWORD_UPDATED, properties)
|
||||||
}
|
}
|
||||||
|
@ -115,6 +152,9 @@ async function passwordUpdated(user: User) {
|
||||||
async function passwordResetRequested(user: User) {
|
async function passwordResetRequested(user: User) {
|
||||||
const properties: UserPasswordResetRequestedEvent = {
|
const properties: UserPasswordResetRequestedEvent = {
|
||||||
userId: user._id as string,
|
userId: user._id as string,
|
||||||
|
audited: {
|
||||||
|
email: user.email,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
await publishEvent(Event.USER_PASSWORD_RESET_REQUESTED, properties)
|
await publishEvent(Event.USER_PASSWORD_RESET_REQUESTED, properties)
|
||||||
}
|
}
|
||||||
|
@ -122,6 +162,9 @@ async function passwordResetRequested(user: User) {
|
||||||
async function passwordReset(user: User) {
|
async function passwordReset(user: User) {
|
||||||
const properties: UserPasswordResetEvent = {
|
const properties: UserPasswordResetEvent = {
|
||||||
userId: user._id as string,
|
userId: user._id as string,
|
||||||
|
audited: {
|
||||||
|
email: user.email,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
await publishEvent(Event.USER_PASSWORD_RESET, properties)
|
await publishEvent(Event.USER_PASSWORD_RESET, properties)
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,11 +21,11 @@ export * as context from "./context"
|
||||||
export * as cache from "./cache"
|
export * as cache from "./cache"
|
||||||
export * as objectStore from "./objectStore"
|
export * as objectStore from "./objectStore"
|
||||||
export * as redis from "./redis"
|
export * as redis from "./redis"
|
||||||
export * as locks from "./redis/redlock"
|
export * as locks from "./redis/redlockImpl"
|
||||||
export * as utils from "./utils"
|
export * as utils from "./utils"
|
||||||
export * as errors from "./errors"
|
export * as errors from "./errors"
|
||||||
export { default as env } from "./environment"
|
export { default as env } from "./environment"
|
||||||
|
export { SearchParams } from "./db"
|
||||||
// Add context to tenancy for backwards compatibility
|
// Add context to tenancy for backwards compatibility
|
||||||
// only do this for external usages to prevent internal
|
// only do this for external usages to prevent internal
|
||||||
// circular dependencies
|
// circular dependencies
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { getGlobalDB, doInTenant } from "../context"
|
||||||
import { decrypt } from "../security/encryption"
|
import { decrypt } from "../security/encryption"
|
||||||
import * as identity from "../context/identity"
|
import * as identity from "../context/identity"
|
||||||
import env from "../environment"
|
import env from "../environment"
|
||||||
import { BBContext, EndpointMatcher } from "@budibase/types"
|
import { Ctx, EndpointMatcher } from "@budibase/types"
|
||||||
|
|
||||||
const ONE_MINUTE = env.SESSION_UPDATE_PERIOD
|
const ONE_MINUTE = env.SESSION_UPDATE_PERIOD
|
||||||
? parseInt(env.SESSION_UPDATE_PERIOD)
|
? parseInt(env.SESSION_UPDATE_PERIOD)
|
||||||
|
@ -73,7 +73,7 @@ export default function (
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const noAuthOptions = noAuthPatterns ? buildMatcherRegex(noAuthPatterns) : []
|
const noAuthOptions = noAuthPatterns ? buildMatcherRegex(noAuthPatterns) : []
|
||||||
return async (ctx: BBContext | any, next: any) => {
|
return async (ctx: Ctx | any, next: any) => {
|
||||||
let publicEndpoint = false
|
let publicEndpoint = false
|
||||||
const version = ctx.request.headers[Header.API_VER]
|
const version = ctx.request.headers[Header.API_VER]
|
||||||
// the path is not authenticated
|
// the path is not authenticated
|
||||||
|
@ -149,7 +149,7 @@ export default function (
|
||||||
finalise(ctx, { authenticated, user, internal, version, publicEndpoint })
|
finalise(ctx, { authenticated, user, internal, version, publicEndpoint })
|
||||||
|
|
||||||
if (user && user.email) {
|
if (user && user.email) {
|
||||||
return identity.doInUserContext(user, next)
|
return identity.doInUserContext(user, ctx, next)
|
||||||
} else {
|
} else {
|
||||||
return next()
|
return next()
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,4 +17,5 @@ export { default as builderOrAdmin } from "./builderOrAdmin"
|
||||||
export { default as builderOnly } from "./builderOnly"
|
export { default as builderOnly } from "./builderOnly"
|
||||||
export { default as logging } from "./logging"
|
export { default as logging } from "./logging"
|
||||||
export { default as errorHandling } from "./errorHandling"
|
export { default as errorHandling } from "./errorHandling"
|
||||||
|
export { default as querystringToBody } from "./querystringToBody"
|
||||||
export * as joiValidator from "./joi-validator"
|
export * as joiValidator from "./joi-validator"
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { Ctx } from "@budibase/types"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expects a standard "query" query string property which is the JSON body
|
||||||
|
* of the request, which has to be sent via query string due to the requirement
|
||||||
|
* of making an endpoint a GET request e.g. downloading a file stream.
|
||||||
|
*/
|
||||||
|
export default function (ctx: Ctx, next: any) {
|
||||||
|
const queryString = ctx.request.query?.query as string | undefined
|
||||||
|
if (ctx.request.method.toLowerCase() !== "get") {
|
||||||
|
ctx.throw(
|
||||||
|
500,
|
||||||
|
"Query to download middleware can only be used for get requests."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (!queryString) {
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
const decoded = decodeURIComponent(queryString)
|
||||||
|
let json
|
||||||
|
try {
|
||||||
|
json = JSON.parse(decoded)
|
||||||
|
} catch (err) {
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
ctx.request.body = json
|
||||||
|
return next()
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
import { StaticDatabases } from "../constants"
|
import { StaticDatabases } from "../constants"
|
||||||
import { getPlatformDB } from "./platformDb"
|
import { getPlatformDB } from "./platformDb"
|
||||||
import { LockName, LockOptions, LockType, Tenants } from "@budibase/types"
|
import { LockName, LockOptions, LockType, Tenants } from "@budibase/types"
|
||||||
import * as locks from "../redis/redlock"
|
import * as locks from "../redis/redlockImpl"
|
||||||
|
|
||||||
const TENANT_DOC = StaticDatabases.PLATFORM_INFO.docs.tenants
|
const TENANT_DOC = StaticDatabases.PLATFORM_INFO.docs.tenants
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
export enum JobQueue {
|
export enum JobQueue {
|
||||||
AUTOMATION = "automationQueue",
|
AUTOMATION = "automationQueue",
|
||||||
APP_BACKUP = "appBackupQueue",
|
APP_BACKUP = "appBackupQueue",
|
||||||
|
AUDIT_LOG = "auditLogQueue",
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,8 +40,10 @@ export function createQueue<T>(
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function shutdown() {
|
export async function shutdown() {
|
||||||
if (QUEUES.length) {
|
if (cleanupInterval) {
|
||||||
clearInterval(cleanupInterval)
|
clearInterval(cleanupInterval)
|
||||||
|
}
|
||||||
|
if (QUEUES.length) {
|
||||||
for (let queue of QUEUES) {
|
for (let queue of QUEUES) {
|
||||||
await queue.close()
|
await queue.close()
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,4 +3,4 @@
|
||||||
export { default as Client } from "./redis"
|
export { default as Client } from "./redis"
|
||||||
export * as utils from "./utils"
|
export * as utils from "./utils"
|
||||||
export * as clients from "./init"
|
export * as clients from "./init"
|
||||||
export * as locks from "./redlock"
|
export * as locks from "./redlockImpl"
|
||||||
|
|
|
@ -10,14 +10,38 @@ import { BulkDocsResponse, User } from "@budibase/types"
|
||||||
import { getGlobalDB } from "./context"
|
import { getGlobalDB } from "./context"
|
||||||
import * as context from "./context"
|
import * as context from "./context"
|
||||||
|
|
||||||
export const bulkGetGlobalUsersById = async (userIds: string[]) => {
|
type GetOpts = { cleanup?: boolean }
|
||||||
|
|
||||||
|
function removeUserPassword(users: User | User[]) {
|
||||||
|
if (Array.isArray(users)) {
|
||||||
|
return users.map(user => {
|
||||||
|
if (user) {
|
||||||
|
delete user.password
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else if (users) {
|
||||||
|
delete users.password
|
||||||
|
return users
|
||||||
|
}
|
||||||
|
return users
|
||||||
|
}
|
||||||
|
|
||||||
|
export const bulkGetGlobalUsersById = async (
|
||||||
|
userIds: string[],
|
||||||
|
opts?: GetOpts
|
||||||
|
) => {
|
||||||
const db = getGlobalDB()
|
const db = getGlobalDB()
|
||||||
return (
|
let users = (
|
||||||
await db.allDocs({
|
await db.allDocs({
|
||||||
keys: userIds,
|
keys: userIds,
|
||||||
include_docs: true,
|
include_docs: true,
|
||||||
})
|
})
|
||||||
).rows.map(row => row.doc) as User[]
|
).rows.map(row => row.doc) as User[]
|
||||||
|
if (opts?.cleanup) {
|
||||||
|
users = removeUserPassword(users) as User[]
|
||||||
|
}
|
||||||
|
return users
|
||||||
}
|
}
|
||||||
|
|
||||||
export const bulkUpdateGlobalUsers = async (users: User[]) => {
|
export const bulkUpdateGlobalUsers = async (users: User[]) => {
|
||||||
|
@ -25,18 +49,22 @@ export const bulkUpdateGlobalUsers = async (users: User[]) => {
|
||||||
return (await db.bulkDocs(users)) as BulkDocsResponse
|
return (await db.bulkDocs(users)) as BulkDocsResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getById(id: string): Promise<User> {
|
export async function getById(id: string, opts?: GetOpts): Promise<User> {
|
||||||
const db = context.getGlobalDB()
|
const db = context.getGlobalDB()
|
||||||
return db.get(id)
|
let user = await db.get(id)
|
||||||
|
if (opts?.cleanup) {
|
||||||
|
user = removeUserPassword(user)
|
||||||
|
}
|
||||||
|
return user
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given an email address this will use a view to search through
|
* Given an email address this will use a view to search through
|
||||||
* all the users to find one with this email address.
|
* all the users to find one with this email address.
|
||||||
* @param {string} email the email to lookup the user by.
|
|
||||||
*/
|
*/
|
||||||
export const getGlobalUserByEmail = async (
|
export const getGlobalUserByEmail = async (
|
||||||
email: String
|
email: String,
|
||||||
|
opts?: GetOpts
|
||||||
): Promise<User | undefined> => {
|
): Promise<User | undefined> => {
|
||||||
if (email == null) {
|
if (email == null) {
|
||||||
throw "Must supply an email address to view"
|
throw "Must supply an email address to view"
|
||||||
|
@ -52,10 +80,19 @@ export const getGlobalUserByEmail = async (
|
||||||
throw new Error(`Multiple users found with email address: ${email}`)
|
throw new Error(`Multiple users found with email address: ${email}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
return response
|
let user = response as User
|
||||||
|
if (opts?.cleanup) {
|
||||||
|
user = removeUserPassword(user) as User
|
||||||
|
}
|
||||||
|
|
||||||
|
return user
|
||||||
}
|
}
|
||||||
|
|
||||||
export const searchGlobalUsersByApp = async (appId: any, opts: any) => {
|
export const searchGlobalUsersByApp = async (
|
||||||
|
appId: any,
|
||||||
|
opts: any,
|
||||||
|
getOpts?: GetOpts
|
||||||
|
) => {
|
||||||
if (typeof appId !== "string") {
|
if (typeof appId !== "string") {
|
||||||
throw new Error("Must provide a string based app ID")
|
throw new Error("Must provide a string based app ID")
|
||||||
}
|
}
|
||||||
|
@ -67,7 +104,11 @@ export const searchGlobalUsersByApp = async (appId: any, opts: any) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
response = []
|
response = []
|
||||||
}
|
}
|
||||||
return Array.isArray(response) ? response : [response]
|
let users: User[] = Array.isArray(response) ? response : [response]
|
||||||
|
if (getOpts?.cleanup) {
|
||||||
|
users = removeUserPassword(users) as User[]
|
||||||
|
}
|
||||||
|
return users
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getGlobalUserByAppPage = (appId: string, user: User) => {
|
export const getGlobalUserByAppPage = (appId: string, user: User) => {
|
||||||
|
@ -80,7 +121,11 @@ export const getGlobalUserByAppPage = (appId: string, user: User) => {
|
||||||
/**
|
/**
|
||||||
* Performs a starts with search on the global email view.
|
* Performs a starts with search on the global email view.
|
||||||
*/
|
*/
|
||||||
export const searchGlobalUsersByEmail = async (email: string, opts: any) => {
|
export const searchGlobalUsersByEmail = async (
|
||||||
|
email: string,
|
||||||
|
opts: any,
|
||||||
|
getOpts?: GetOpts
|
||||||
|
) => {
|
||||||
if (typeof email !== "string") {
|
if (typeof email !== "string") {
|
||||||
throw new Error("Must provide a string to search by")
|
throw new Error("Must provide a string to search by")
|
||||||
}
|
}
|
||||||
|
@ -95,5 +140,9 @@ export const searchGlobalUsersByEmail = async (email: string, opts: any) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
response = []
|
response = []
|
||||||
}
|
}
|
||||||
return Array.isArray(response) ? response : [response]
|
let users: User[] = Array.isArray(response) ? response : [response]
|
||||||
|
if (getOpts?.cleanup) {
|
||||||
|
users = removeUserPassword(users) as User[]
|
||||||
|
}
|
||||||
|
return users
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,13 @@ import {
|
||||||
import env from "../environment"
|
import env from "../environment"
|
||||||
import * as tenancy from "../tenancy"
|
import * as tenancy from "../tenancy"
|
||||||
import * as context from "../context"
|
import * as context from "../context"
|
||||||
import { App, Ctx, TenantResolutionStrategy } from "@budibase/types"
|
import {
|
||||||
|
App,
|
||||||
|
AuditedEventFriendlyName,
|
||||||
|
Ctx,
|
||||||
|
Event,
|
||||||
|
TenantResolutionStrategy,
|
||||||
|
} from "@budibase/types"
|
||||||
import { SetOption } from "cookies"
|
import { SetOption } from "cookies"
|
||||||
const jwt = require("jsonwebtoken")
|
const jwt = require("jsonwebtoken")
|
||||||
|
|
||||||
|
@ -217,3 +223,7 @@ export async function getBuildersCount() {
|
||||||
export function timeout(timeMs: number) {
|
export function timeout(timeMs: number) {
|
||||||
return new Promise(resolve => setTimeout(resolve, timeMs))
|
return new Promise(resolve => setTimeout(resolve, timeMs))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isAudited(event: Event) {
|
||||||
|
return !!AuditedEventFriendlyName[event]
|
||||||
|
}
|
||||||
|
|
|
@ -82,6 +82,10 @@ export const useEnvironmentVariables = () => {
|
||||||
return useFeature(Feature.ENVIRONMENT_VARIABLES)
|
return useFeature(Feature.ENVIRONMENT_VARIABLES)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const useAuditLogs = () => {
|
||||||
|
return useFeature(Feature.AUDIT_LOGS)
|
||||||
|
}
|
||||||
|
|
||||||
// QUOTAS
|
// QUOTAS
|
||||||
|
|
||||||
export const setAutomationLogsQuota = (value: number) => {
|
export const setAutomationLogsQuota = (value: number) => {
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
import Chance from "chance"
|
||||||
|
export const generator = new Chance()
|
|
@ -1,8 +1,4 @@
|
||||||
export * from "./common"
|
export * from "./common"
|
||||||
|
|
||||||
import Chance from "chance"
|
|
||||||
export const generator = new Chance()
|
|
||||||
|
|
||||||
export * as accounts from "./accounts"
|
export * as accounts from "./accounts"
|
||||||
export * as apps from "./apps"
|
export * as apps from "./apps"
|
||||||
export * as db from "./db"
|
export * as db from "./db"
|
||||||
|
@ -12,3 +8,4 @@ export * as plugins from "./plugins"
|
||||||
export * as sso from "./sso"
|
export * as sso from "./sso"
|
||||||
export * as tenant from "./tenants"
|
export * as tenant from "./tenants"
|
||||||
export * as users from "./users"
|
export * as users from "./users"
|
||||||
|
export { generator } from "./generator"
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { User } from "@budibase/types"
|
||||||
|
import { generator } from "./generator"
|
||||||
|
import { uuid } from "./common"
|
||||||
|
|
||||||
|
export const newEmail = () => {
|
||||||
|
return `${uuid()}@test.com`
|
||||||
|
}
|
||||||
|
|
||||||
|
export const user = (userProps?: any): User => {
|
||||||
|
return {
|
||||||
|
email: newEmail(),
|
||||||
|
password: "test",
|
||||||
|
roles: { app_test: "admin" },
|
||||||
|
firstName: generator.first(),
|
||||||
|
lastName: generator.last(),
|
||||||
|
pictureUrl: "http://test.com",
|
||||||
|
...userProps,
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,8 +8,36 @@ import {
|
||||||
SSOProviderType,
|
SSOProviderType,
|
||||||
User,
|
User,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { uuid, generator, users, email } from "./index"
|
import { generator } from "./generator"
|
||||||
|
import { uuid, email } from "./common"
|
||||||
|
import * as shared from "./shared"
|
||||||
import _ from "lodash"
|
import _ from "lodash"
|
||||||
|
import { user } from "./shared"
|
||||||
|
|
||||||
|
export function authDetails(userDoc?: User): SSOAuthDetails {
|
||||||
|
if (!userDoc) {
|
||||||
|
userDoc = user()
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = userDoc._id || uuid()
|
||||||
|
const provider = generator.string()
|
||||||
|
|
||||||
|
const profile = ssoProfile(userDoc)
|
||||||
|
profile.provider = provider
|
||||||
|
profile.id = userId
|
||||||
|
|
||||||
|
return {
|
||||||
|
email: userDoc.email,
|
||||||
|
oauth2: {
|
||||||
|
refreshToken: generator.string(),
|
||||||
|
accessToken: generator.string(),
|
||||||
|
},
|
||||||
|
profile,
|
||||||
|
provider,
|
||||||
|
providerType: providerType(),
|
||||||
|
userId,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function providerType(): SSOProviderType {
|
export function providerType(): SSOProviderType {
|
||||||
return _.sample(Object.values(SSOProviderType)) as SSOProviderType
|
return _.sample(Object.values(SSOProviderType)) as SSOProviderType
|
||||||
|
@ -17,7 +45,7 @@ export function providerType(): SSOProviderType {
|
||||||
|
|
||||||
export function ssoProfile(user?: User): SSOProfile {
|
export function ssoProfile(user?: User): SSOProfile {
|
||||||
if (!user) {
|
if (!user) {
|
||||||
user = users.user()
|
user = shared.user()
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
id: user._id!,
|
id: user._id!,
|
||||||
|
@ -33,31 +61,6 @@ export function ssoProfile(user?: User): SSOProfile {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function authDetails(user?: User): SSOAuthDetails {
|
|
||||||
if (!user) {
|
|
||||||
user = users.user()
|
|
||||||
}
|
|
||||||
|
|
||||||
const userId = user._id || uuid()
|
|
||||||
const provider = generator.string()
|
|
||||||
|
|
||||||
const profile = ssoProfile(user)
|
|
||||||
profile.provider = provider
|
|
||||||
profile.id = userId
|
|
||||||
|
|
||||||
return {
|
|
||||||
email: user.email,
|
|
||||||
oauth2: {
|
|
||||||
refreshToken: generator.string(),
|
|
||||||
accessToken: generator.string(),
|
|
||||||
},
|
|
||||||
profile,
|
|
||||||
provider,
|
|
||||||
providerType: providerType(),
|
|
||||||
userId,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// OIDC
|
// OIDC
|
||||||
|
|
||||||
export function oidcConfig(): OIDCInnerConfig {
|
export function oidcConfig(): OIDCInnerConfig {
|
||||||
|
|
|
@ -1,29 +1,13 @@
|
||||||
import { generator } from "../"
|
|
||||||
import {
|
import {
|
||||||
AdminUser,
|
AdminUser,
|
||||||
BuilderUser,
|
BuilderUser,
|
||||||
SSOAuthDetails,
|
SSOAuthDetails,
|
||||||
SSOUser,
|
SSOUser,
|
||||||
User,
|
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { v4 as uuid } from "uuid"
|
import { user } from "./shared"
|
||||||
import * as sso from "./sso"
|
import { authDetails } from "./sso"
|
||||||
|
|
||||||
export const newEmail = () => {
|
export { user, newEmail } from "./shared"
|
||||||
return `${uuid()}@test.com`
|
|
||||||
}
|
|
||||||
|
|
||||||
export const user = (userProps?: any): User => {
|
|
||||||
return {
|
|
||||||
email: newEmail(),
|
|
||||||
password: "test",
|
|
||||||
roles: { app_test: "admin" },
|
|
||||||
firstName: generator.first(),
|
|
||||||
lastName: generator.last(),
|
|
||||||
pictureUrl: "http://test.com",
|
|
||||||
...userProps,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const adminUser = (userProps?: any): AdminUser => {
|
export const adminUser = (userProps?: any): AdminUser => {
|
||||||
return {
|
return {
|
||||||
|
@ -53,7 +37,7 @@ export function ssoUser(
|
||||||
delete base.password
|
delete base.password
|
||||||
|
|
||||||
if (!opts.details) {
|
if (!opts.details) {
|
||||||
opts.details = sso.authDetails(base)
|
opts.details = authDetails(base)
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -1,3 +1,31 @@
|
||||||
|
import { execSync } from "child_process"
|
||||||
|
|
||||||
|
let dockerPsResult: string | undefined
|
||||||
|
|
||||||
|
function formatDockerPsResult(serverName: string, port: number) {
|
||||||
|
const lines = dockerPsResult?.split("\n")
|
||||||
|
let first = true
|
||||||
|
if (!lines) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
for (let line of lines) {
|
||||||
|
if (first) {
|
||||||
|
first = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
let toLookFor = serverName.split("-service")[0]
|
||||||
|
if (!line.includes(toLookFor)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const regex = new RegExp(`0.0.0.0:([0-9]*)->${port}`, "g")
|
||||||
|
const found = line.match(regex)
|
||||||
|
if (found) {
|
||||||
|
return found[0].split(":")[1].split("->")[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
function getTestContainerSettings(
|
function getTestContainerSettings(
|
||||||
serverName: string,
|
serverName: string,
|
||||||
key: string
|
key: string
|
||||||
|
@ -14,10 +42,22 @@ function getTestContainerSettings(
|
||||||
}
|
}
|
||||||
|
|
||||||
function getContainerInfo(containerName: string, port: number) {
|
function getContainerInfo(containerName: string, port: number) {
|
||||||
const assignedPort = getTestContainerSettings(
|
let assignedPort = getTestContainerSettings(
|
||||||
containerName.toUpperCase(),
|
containerName.toUpperCase(),
|
||||||
`PORT_${port}`
|
`PORT_${port}`
|
||||||
)
|
)
|
||||||
|
if (!dockerPsResult) {
|
||||||
|
try {
|
||||||
|
const outputBuffer = execSync("docker ps")
|
||||||
|
dockerPsResult = outputBuffer.toString("utf8")
|
||||||
|
} catch (err) {
|
||||||
|
//no-op
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const possiblePort = formatDockerPsResult(containerName, port)
|
||||||
|
if (possiblePort) {
|
||||||
|
assignedPort = possiblePort
|
||||||
|
}
|
||||||
const host = getTestContainerSettings(containerName.toUpperCase(), "IP")
|
const host = getTestContainerSettings(containerName.toUpperCase(), "IP")
|
||||||
return {
|
return {
|
||||||
port: assignedPort,
|
port: assignedPort,
|
||||||
|
@ -39,12 +79,15 @@ function getRedisConfig() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setupEnv(...envs: any[]) {
|
export function setupEnv(...envs: any[]) {
|
||||||
|
const couch = getCouchConfig(),
|
||||||
|
minio = getCouchConfig(),
|
||||||
|
redis = getRedisConfig()
|
||||||
const configs = [
|
const configs = [
|
||||||
{ key: "COUCH_DB_PORT", value: getCouchConfig().port },
|
{ key: "COUCH_DB_PORT", value: couch.port },
|
||||||
{ key: "COUCH_DB_URL", value: getCouchConfig().url },
|
{ key: "COUCH_DB_URL", value: couch.url },
|
||||||
{ key: "MINIO_PORT", value: getMinioConfig().port },
|
{ key: "MINIO_PORT", value: minio.port },
|
||||||
{ key: "MINIO_URL", value: getMinioConfig().url },
|
{ key: "MINIO_URL", value: minio.url },
|
||||||
{ key: "REDIS_URL", value: getRedisConfig().url },
|
{ key: "REDIS_URL", value: redis.url },
|
||||||
]
|
]
|
||||||
|
|
||||||
for (const config of configs.filter(x => !!x.value)) {
|
for (const config of configs.filter(x => !!x.value)) {
|
||||||
|
|
|
@ -14,6 +14,8 @@
|
||||||
export let autocomplete = false
|
export let autocomplete = false
|
||||||
export let sort = false
|
export let sort = false
|
||||||
export let autoWidth = false
|
export let autoWidth = false
|
||||||
|
export let fetchTerm = null
|
||||||
|
export let customPopoverHeight
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
@ -83,10 +85,12 @@
|
||||||
{options}
|
{options}
|
||||||
isPlaceholder={!value?.length}
|
isPlaceholder={!value?.length}
|
||||||
{autocomplete}
|
{autocomplete}
|
||||||
|
bind:fetchTerm
|
||||||
{isOptionSelected}
|
{isOptionSelected}
|
||||||
{getOptionLabel}
|
{getOptionLabel}
|
||||||
{getOptionValue}
|
{getOptionValue}
|
||||||
onSelectOption={toggleOption}
|
onSelectOption={toggleOption}
|
||||||
{sort}
|
{sort}
|
||||||
{autoWidth}
|
{autoWidth}
|
||||||
|
{customPopoverHeight}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -31,7 +31,8 @@
|
||||||
export let autoWidth = false
|
export let autoWidth = false
|
||||||
export let autocomplete = false
|
export let autocomplete = false
|
||||||
export let sort = false
|
export let sort = false
|
||||||
|
export let fetchTerm = null
|
||||||
|
export let customPopoverHeight
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
let searchTerm = null
|
let searchTerm = null
|
||||||
|
@ -71,7 +72,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const getFilteredOptions = (options, term, getLabel) => {
|
const getFilteredOptions = (options, term, getLabel) => {
|
||||||
if (autocomplete && term) {
|
if (autocomplete && term && !fetchTerm) {
|
||||||
const lowerCaseTerm = term.toLowerCase()
|
const lowerCaseTerm = term.toLowerCase()
|
||||||
return options.filter(option => {
|
return options.filter(option => {
|
||||||
return `${getLabel(option)}`.toLowerCase().includes(lowerCaseTerm)
|
return `${getLabel(option)}`.toLowerCase().includes(lowerCaseTerm)
|
||||||
|
@ -136,6 +137,7 @@
|
||||||
on:close={() => (open = false)}
|
on:close={() => (open = false)}
|
||||||
useAnchorWidth={!autoWidth}
|
useAnchorWidth={!autoWidth}
|
||||||
maxWidth={autoWidth ? 400 : null}
|
maxWidth={autoWidth ? 400 : null}
|
||||||
|
customHeight={customPopoverHeight}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="popover-content"
|
class="popover-content"
|
||||||
|
@ -144,8 +146,9 @@
|
||||||
>
|
>
|
||||||
{#if autocomplete}
|
{#if autocomplete}
|
||||||
<Search
|
<Search
|
||||||
value={searchTerm}
|
value={fetchTerm ? fetchTerm : searchTerm}
|
||||||
on:change={event => (searchTerm = event.detail)}
|
on:change={event =>
|
||||||
|
fetchTerm ? (fetchTerm = event.detail) : (searchTerm = event.detail)}
|
||||||
{disabled}
|
{disabled}
|
||||||
placeholder="Search"
|
placeholder="Search"
|
||||||
/>
|
/>
|
||||||
|
@ -247,7 +250,7 @@
|
||||||
}
|
}
|
||||||
.popover-content.auto-width .spectrum-Menu-itemLabel {
|
.popover-content.auto-width .spectrum-Menu-itemLabel {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: none;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
.popover-content:not(.auto-width) .spectrum-Menu-itemLabel {
|
.popover-content:not(.auto-width) .spectrum-Menu-itemLabel {
|
||||||
|
|
|
@ -15,6 +15,10 @@
|
||||||
export let getOptionValue = option => option
|
export let getOptionValue = option => option
|
||||||
export let sort = false
|
export let sort = false
|
||||||
export let autoWidth = false
|
export let autoWidth = false
|
||||||
|
export let autocomplete = false
|
||||||
|
export let fetchTerm = null
|
||||||
|
export let customPopoverHeight
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
const onChange = e => {
|
const onChange = e => {
|
||||||
value = e.detail
|
value = e.detail
|
||||||
|
@ -34,6 +38,9 @@
|
||||||
{getOptionLabel}
|
{getOptionLabel}
|
||||||
{getOptionValue}
|
{getOptionValue}
|
||||||
{autoWidth}
|
{autoWidth}
|
||||||
|
{autocomplete}
|
||||||
|
{customPopoverHeight}
|
||||||
|
bind:fetchTerm
|
||||||
on:change={onChange}
|
on:change={onChange}
|
||||||
on:click
|
on:click
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -20,6 +20,8 @@
|
||||||
export let autoWidth = false
|
export let autoWidth = false
|
||||||
export let sort = false
|
export let sort = false
|
||||||
export let tooltip = ""
|
export let tooltip = ""
|
||||||
|
export let autocomplete = false
|
||||||
|
export let customPopoverHeight
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
const onChange = e => {
|
const onChange = e => {
|
||||||
|
@ -51,6 +53,8 @@
|
||||||
{getOptionIcon}
|
{getOptionIcon}
|
||||||
{getOptionColour}
|
{getOptionColour}
|
||||||
{isOptionEnabled}
|
{isOptionEnabled}
|
||||||
|
{autocomplete}
|
||||||
|
{customPopoverHeight}
|
||||||
on:change={onChange}
|
on:change={onChange}
|
||||||
on:click
|
on:click
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
export let useAnchorWidth = false
|
export let useAnchorWidth = false
|
||||||
export let dismissible = true
|
export let dismissible = true
|
||||||
export let offset = 5
|
export let offset = 5
|
||||||
|
export let customHeight
|
||||||
|
|
||||||
$: target = portalTarget || getContext(Context.PopoverRoot) || ".spectrum"
|
$: target = portalTarget || getContext(Context.PopoverRoot) || ".spectrum"
|
||||||
|
|
||||||
|
@ -74,6 +75,7 @@
|
||||||
on:keydown={handleEscape}
|
on:keydown={handleEscape}
|
||||||
class="spectrum-Popover is-open"
|
class="spectrum-Popover is-open"
|
||||||
role="presentation"
|
role="presentation"
|
||||||
|
style="height: {customHeight}"
|
||||||
transition:fly|local={{ y: -20, duration: 200 }}
|
transition:fly|local={{ y: -20, duration: 200 }}
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
|
|
|
@ -0,0 +1,76 @@
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
Layout,
|
||||||
|
Heading,
|
||||||
|
Body,
|
||||||
|
Button,
|
||||||
|
Divider,
|
||||||
|
Tags,
|
||||||
|
Tag,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import { auth, admin } from "stores/portal"
|
||||||
|
|
||||||
|
export let title
|
||||||
|
export let planType
|
||||||
|
export let description
|
||||||
|
export let enabled
|
||||||
|
export let upgradeButtonClick
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Layout noPadding>
|
||||||
|
<Layout gap="XS" noPadding>
|
||||||
|
<div class="title">
|
||||||
|
<Heading size="M">{title}</Heading>
|
||||||
|
{#if !enabled}
|
||||||
|
<Tags>
|
||||||
|
<Tag icon="LockClosed">{planType}</Tag>
|
||||||
|
</Tags>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<Body>{description}</Body>
|
||||||
|
</Layout>
|
||||||
|
<Divider size="S" />
|
||||||
|
|
||||||
|
{#if enabled}
|
||||||
|
<slot />
|
||||||
|
{:else}
|
||||||
|
<div class="buttons">
|
||||||
|
<Button
|
||||||
|
primary
|
||||||
|
disabled={!$auth.accountPortalAccess && $admin.cloud}
|
||||||
|
on:click={async () => upgradeButtonClick()}
|
||||||
|
>
|
||||||
|
Upgrade
|
||||||
|
</Button>
|
||||||
|
<!--Show the view plans button-->
|
||||||
|
<Button
|
||||||
|
secondary
|
||||||
|
on:click={() => {
|
||||||
|
window.open("https://budibase.com/pricing/", "_blank")
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
View Plans
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</Layout>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-l);
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -4,12 +4,13 @@
|
||||||
import { Content, SideNav, SideNavItem } from "components/portal/page"
|
import { Content, SideNav, SideNavItem } from "components/portal/page"
|
||||||
import { menu } from "stores/portal"
|
import { menu } from "stores/portal"
|
||||||
|
|
||||||
|
$: wide = $isActive("./auditLogs")
|
||||||
$: pages = $menu.find(x => x.title === "Account")?.subPages || []
|
$: pages = $menu.find(x => x.title === "Account")?.subPages || []
|
||||||
$: !pages.length && $goto("../")
|
$: !pages.length && $goto("../")
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Page>
|
<Page>
|
||||||
<Content narrow>
|
<Content narrow={!wide}>
|
||||||
<div slot="side-nav">
|
<div slot="side-nav">
|
||||||
<SideNav>
|
<SideNav>
|
||||||
{#each pages as { title, href }}
|
{#each pages as { title, href }}
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
<script>
|
||||||
|
export let value
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>{value?.name || ""}</div>
|
|
@ -0,0 +1,11 @@
|
||||||
|
<script>
|
||||||
|
import dayjs from "dayjs"
|
||||||
|
import relativeTime from "dayjs/plugin/relativeTime"
|
||||||
|
dayjs.extend(relativeTime)
|
||||||
|
|
||||||
|
export let row
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{dayjs(row.timestamp).fromNow()}
|
||||||
|
</div>
|
|
@ -0,0 +1,45 @@
|
||||||
|
<script>
|
||||||
|
import { Avatar, Tooltip } from "@budibase/bbui"
|
||||||
|
export let row
|
||||||
|
|
||||||
|
let showTooltip
|
||||||
|
const getInitials = user => {
|
||||||
|
let initials = ""
|
||||||
|
initials += user.firstName ? user.firstName[0] : ""
|
||||||
|
initials += user.lastName ? user.lastName[0] : ""
|
||||||
|
|
||||||
|
return initials === "" ? user.email[0] : initials
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="container"
|
||||||
|
on:mouseover={() => (showTooltip = true)}
|
||||||
|
on:focus={() => (showTooltip = true)}
|
||||||
|
on:mouseleave={() => (showTooltip = false)}
|
||||||
|
>
|
||||||
|
<Avatar size="M" initials={getInitials(row?.user)} />
|
||||||
|
</div>
|
||||||
|
{#if showTooltip}
|
||||||
|
<div class="tooltip">
|
||||||
|
<Tooltip textWrapping text={row?.user.email} direction="bottom" />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.tooltip {
|
||||||
|
z-index: 1;
|
||||||
|
position: absolute;
|
||||||
|
top: 75%;
|
||||||
|
left: 120%;
|
||||||
|
transform: translateX(-100%) translateY(-50%);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-end;
|
||||||
|
width: 130px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,13 @@
|
||||||
|
<script>
|
||||||
|
import { ActionButton } from "@budibase/bbui"
|
||||||
|
import { getContext } from "svelte"
|
||||||
|
|
||||||
|
export let row
|
||||||
|
const auditLogs = getContext("auditLogs")
|
||||||
|
const onClick = e => {
|
||||||
|
e.stopPropagation()
|
||||||
|
auditLogs.viewDetails(row)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ActionButton size="S" on:click={onClick}>Details</ActionButton>
|
|
@ -0,0 +1,501 @@
|
||||||
|
<!-- If working on this file, you may notice that if you click the download button in the UI
|
||||||
|
hot reload will stop working due to the use of window.location. You'll need to reload the pag
|
||||||
|
to get it working again.
|
||||||
|
-->
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
Layout,
|
||||||
|
Table,
|
||||||
|
Search,
|
||||||
|
Multiselect,
|
||||||
|
notifications,
|
||||||
|
Icon,
|
||||||
|
clickOutside,
|
||||||
|
CoreTextArea,
|
||||||
|
DatePicker,
|
||||||
|
Pagination,
|
||||||
|
Helpers,
|
||||||
|
Divider,
|
||||||
|
ActionButton,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import { licensing, users, apps, auditLogs } from "stores/portal"
|
||||||
|
import LockedFeature from "../../_components/LockedFeature.svelte"
|
||||||
|
import { createPaginationStore } from "helpers/pagination"
|
||||||
|
import { onMount, setContext } from "svelte"
|
||||||
|
import ViewDetailsRenderer from "./_components/ViewDetailsRenderer.svelte"
|
||||||
|
import UserRenderer from "./_components/UserRenderer.svelte"
|
||||||
|
import TimeRenderer from "./_components/TimeRenderer.svelte"
|
||||||
|
import AppColumnRenderer from "./_components/AppColumnRenderer.svelte"
|
||||||
|
import { cloneDeep } from "lodash"
|
||||||
|
|
||||||
|
const schema = {
|
||||||
|
date: { width: "0.8fr" },
|
||||||
|
user: { width: "0.5fr" },
|
||||||
|
name: { width: "2fr", displayName: "Event" },
|
||||||
|
app: { width: "1.5fr" },
|
||||||
|
view: { width: "0.1fr", borderLeft: true, displayName: "" },
|
||||||
|
}
|
||||||
|
|
||||||
|
const customRenderers = [
|
||||||
|
{
|
||||||
|
column: "view",
|
||||||
|
component: ViewDetailsRenderer,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
column: "user",
|
||||||
|
component: UserRenderer,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
column: "date",
|
||||||
|
component: TimeRenderer,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
column: "app",
|
||||||
|
component: AppColumnRenderer,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
let userSearchTerm = ""
|
||||||
|
let logSearchTerm = ""
|
||||||
|
let userPageInfo = createPaginationStore()
|
||||||
|
let logsPageInfo = createPaginationStore()
|
||||||
|
|
||||||
|
let prevUserSearch = undefined
|
||||||
|
let prevLogSearch = undefined
|
||||||
|
let selectedUsers = []
|
||||||
|
let selectedApps = []
|
||||||
|
let selectedEvents = []
|
||||||
|
let selectedLog
|
||||||
|
let sidePanelVisible = false
|
||||||
|
let wideSidePanel = false
|
||||||
|
let timer
|
||||||
|
let startDate = new Date()
|
||||||
|
startDate.setDate(startDate.getDate() - 30)
|
||||||
|
let endDate = new Date()
|
||||||
|
|
||||||
|
$: fetchUsers(userPage, userSearchTerm)
|
||||||
|
$: fetchLogs({
|
||||||
|
logsPage,
|
||||||
|
logSearchTerm,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
selectedUsers,
|
||||||
|
selectedApps,
|
||||||
|
selectedEvents,
|
||||||
|
})
|
||||||
|
$: userPage = $userPageInfo.page
|
||||||
|
$: logsPage = $logsPageInfo.page
|
||||||
|
|
||||||
|
$: sortedUsers = sort(
|
||||||
|
enrich($users.data || [], selectedUsers, "_id"),
|
||||||
|
"email"
|
||||||
|
)
|
||||||
|
$: sortedEvents = sort(
|
||||||
|
enrich(parseEventObject($auditLogs.events), selectedEvents, "id"),
|
||||||
|
"id"
|
||||||
|
)
|
||||||
|
$: sortedApps = sort(enrich($apps, selectedApps, "appId"), "name")
|
||||||
|
|
||||||
|
const debounce = value => {
|
||||||
|
clearTimeout(timer)
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
logSearchTerm = value
|
||||||
|
}, 400)
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchUsers = async (userPage, search) => {
|
||||||
|
if ($userPageInfo.loading) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// need to remove the page if they've started searching
|
||||||
|
if (search && !prevUserSearch) {
|
||||||
|
userPageInfo.reset()
|
||||||
|
userPage = undefined
|
||||||
|
}
|
||||||
|
prevUserSearch = search
|
||||||
|
try {
|
||||||
|
userPageInfo.loading()
|
||||||
|
await users.search({ userPage, email: search })
|
||||||
|
userPageInfo.fetched($users.hasNextPage, $users.nextPage)
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error("Error getting user list")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchLogs = async ({
|
||||||
|
logsPage,
|
||||||
|
logSearchTerm,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
selectedUsers,
|
||||||
|
selectedApps,
|
||||||
|
selectedEvents,
|
||||||
|
}) => {
|
||||||
|
if ($logsPageInfo.loading) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// need to remove the page if they've started searching
|
||||||
|
if (logSearchTerm && !prevLogSearch) {
|
||||||
|
logsPageInfo.reset()
|
||||||
|
logsPage = undefined
|
||||||
|
}
|
||||||
|
prevLogSearch = logSearchTerm
|
||||||
|
try {
|
||||||
|
logsPageInfo.loading()
|
||||||
|
await auditLogs.search({
|
||||||
|
bookmark: logsPage,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
fullSearch: logSearchTerm,
|
||||||
|
userIds: selectedUsers,
|
||||||
|
appIds: selectedApps,
|
||||||
|
events: selectedEvents,
|
||||||
|
})
|
||||||
|
logsPageInfo.fetched(
|
||||||
|
$auditLogs.logs.hasNextPage,
|
||||||
|
$auditLogs.logs.bookmark
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error(`Error getting audit logs - ${error}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const enrich = (list, selected, key) => {
|
||||||
|
return list.map(item => {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
selected:
|
||||||
|
selected.find(x => x === item[key] || x.includes(item[key])) != null,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const sort = (list, key) => {
|
||||||
|
let sortedList = list.slice()
|
||||||
|
sortedList?.sort((a, b) => {
|
||||||
|
if (a.selected === b.selected) {
|
||||||
|
return a[key] < b[key] ? -1 : 1
|
||||||
|
} else if (a.selected) {
|
||||||
|
return -1
|
||||||
|
} else if (b.selected) {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
})
|
||||||
|
return sortedList
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseEventObject = obj => {
|
||||||
|
// convert obj which is an object of key value pairs to an array of objects
|
||||||
|
// with the key as the id and the value as the name
|
||||||
|
if (obj) {
|
||||||
|
return Object.entries(obj).map(([id, label]) => {
|
||||||
|
return { id, label }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewDetails = detail => {
|
||||||
|
selectedLog = detail
|
||||||
|
sidePanelVisible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const downloadLogs = async () => {
|
||||||
|
try {
|
||||||
|
window.location = auditLogs.getDownloadUrl({
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
fullSearch: logSearchTerm,
|
||||||
|
userIds: selectedUsers,
|
||||||
|
appIds: selectedApps,
|
||||||
|
events: selectedEvents,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error(`Error downloading logs: ` + error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setContext("auditLogs", {
|
||||||
|
viewDetails,
|
||||||
|
})
|
||||||
|
|
||||||
|
const copyToClipboard = async value => {
|
||||||
|
await Helpers.copyToClipboard(value)
|
||||||
|
notifications.success("Copied")
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanupMetadata(log) {
|
||||||
|
const cloned = cloneDeep(log)
|
||||||
|
cloned.userId = cloned.user._id
|
||||||
|
if (cloned.app) {
|
||||||
|
cloned.appId = cloned.app.appId
|
||||||
|
}
|
||||||
|
// remove props that are confused/not returned in download
|
||||||
|
delete cloned._id
|
||||||
|
delete cloned._rev
|
||||||
|
delete cloned.app
|
||||||
|
delete cloned.user
|
||||||
|
return cloned
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
await auditLogs.getEventDefinitions()
|
||||||
|
await licensing.init()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<LockedFeature
|
||||||
|
title={"Audit Logs"}
|
||||||
|
planType={"Business plan"}
|
||||||
|
description={"View all events that have occurred in your Budibase installation"}
|
||||||
|
enabled={$licensing.auditLogsEnabled}
|
||||||
|
upgradeButtonClick={async () => {
|
||||||
|
$licensing.goToUpgradePage()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="controls">
|
||||||
|
<div class="select">
|
||||||
|
<Multiselect
|
||||||
|
bind:fetchTerm={userSearchTerm}
|
||||||
|
placeholder="All users"
|
||||||
|
label="Users"
|
||||||
|
autocomplete
|
||||||
|
bind:value={selectedUsers}
|
||||||
|
getOptionValue={user => user._id}
|
||||||
|
getOptionLabel={user => user.email}
|
||||||
|
options={sortedUsers}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="select">
|
||||||
|
<Multiselect
|
||||||
|
autocomplete
|
||||||
|
placeholder="All apps"
|
||||||
|
label="Apps"
|
||||||
|
getOptionValue={app => app.instance._id}
|
||||||
|
getOptionLabel={app => app.name}
|
||||||
|
options={sortedApps}
|
||||||
|
bind:value={selectedApps}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="select">
|
||||||
|
<Multiselect
|
||||||
|
customPopoverHeight="500px"
|
||||||
|
autocomplete
|
||||||
|
getOptionValue={event => event.id}
|
||||||
|
getOptionLabel={event => event.label}
|
||||||
|
options={sortedEvents}
|
||||||
|
placeholder="All events"
|
||||||
|
label="Events"
|
||||||
|
bind:value={selectedEvents}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="date-picker">
|
||||||
|
<DatePicker
|
||||||
|
value={[startDate, endDate]}
|
||||||
|
placeholder="Choose date range"
|
||||||
|
range={true}
|
||||||
|
on:change={e => {
|
||||||
|
if (e.detail[0]?.length === 1) {
|
||||||
|
startDate = e.detail[0][0].toISOString()
|
||||||
|
endDate = ""
|
||||||
|
} else if (e.detail[0]?.length > 1) {
|
||||||
|
startDate = e.detail[0][0].toISOString()
|
||||||
|
endDate = e.detail[0][1].toISOString()
|
||||||
|
} else {
|
||||||
|
startDate = ""
|
||||||
|
endDate = ""
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="freeSearch">
|
||||||
|
<Search placeholder="Search" on:change={e => debounce(e.detail)} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="">
|
||||||
|
<ActionButton size="M" icon="Download" on:click={() => downloadLogs()} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Layout noPadding>
|
||||||
|
<Table
|
||||||
|
on:click={({ detail }) => viewDetails(detail)}
|
||||||
|
{customRenderers}
|
||||||
|
data={$auditLogs.logs.data}
|
||||||
|
allowEditColumns={false}
|
||||||
|
allowEditRows={false}
|
||||||
|
allowSelectRows={false}
|
||||||
|
{schema}
|
||||||
|
/>
|
||||||
|
<div class="pagination">
|
||||||
|
<Pagination
|
||||||
|
page={$logsPageInfo.pageNumber}
|
||||||
|
hasPrevPage={$logsPageInfo.loading ? false : $logsPageInfo.hasPrevPage}
|
||||||
|
hasNextPage={$logsPageInfo.loading ? false : $logsPageInfo.hasNextPage}
|
||||||
|
goToPrevPage={logsPageInfo.prevPage}
|
||||||
|
goToNextPage={logsPageInfo.nextPage}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
</LockedFeature>
|
||||||
|
|
||||||
|
{#if selectedLog}
|
||||||
|
<div
|
||||||
|
id="side-panel"
|
||||||
|
class:wide={wideSidePanel}
|
||||||
|
class:visible={sidePanelVisible}
|
||||||
|
use:clickOutside={() => {
|
||||||
|
sidePanelVisible = false
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="side-panel-header">
|
||||||
|
Audit Log
|
||||||
|
<div class="side-panel-icons">
|
||||||
|
<Icon
|
||||||
|
size="S"
|
||||||
|
hoverable
|
||||||
|
name={wideSidePanel ? "Minimize" : "Maximize"}
|
||||||
|
on:click={() => {
|
||||||
|
wideSidePanel = !wideSidePanel
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Icon
|
||||||
|
hoverable
|
||||||
|
name="Close"
|
||||||
|
on:click={() => {
|
||||||
|
sidePanelVisible = false
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<div class="side-panel-body">
|
||||||
|
<div
|
||||||
|
on:click={() => copyToClipboard(JSON.stringify(selectedLog.metadata))}
|
||||||
|
class="copy-icon"
|
||||||
|
>
|
||||||
|
<Icon name="Copy" size="S" />
|
||||||
|
</div>
|
||||||
|
<CoreTextArea
|
||||||
|
disabled
|
||||||
|
minHeight={"300px"}
|
||||||
|
height={"100%"}
|
||||||
|
value={JSON.stringify(cleanupMetadata(selectedLog), null, 2)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.copy-icon {
|
||||||
|
right: 16px;
|
||||||
|
top: 80px;
|
||||||
|
z-index: 10;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
border: 1px solid var(--spectrum-alias-border-color);
|
||||||
|
border-radius: var(--spectrum-alias-border-radius-regular);
|
||||||
|
width: 31px;
|
||||||
|
color: var(--spectrum-alias-text-color);
|
||||||
|
background-color: var(--spectrum-global-color-gray-75);
|
||||||
|
transition: background-color
|
||||||
|
var(--spectrum-global-animation-duration-100, 130ms),
|
||||||
|
box-shadow var(--spectrum-global-animation-duration-100, 130ms),
|
||||||
|
border-color var(--spectrum-global-animation-duration-100, 130ms);
|
||||||
|
height: calc(var(--spectrum-alias-item-height-m) - 2px);
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
.copy-icon:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--spectrum-alias-text-color-hover);
|
||||||
|
background-color: var(--spectrum-global-color-gray-50);
|
||||||
|
border-color: var(--spectrum-alias-border-color-hover);
|
||||||
|
}
|
||||||
|
.side-panel-header {
|
||||||
|
display: flex;
|
||||||
|
padding: 20px 10px 10px 10px;
|
||||||
|
gap: var(--spacing-s);
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-panel-body {
|
||||||
|
padding: 10px;
|
||||||
|
height: calc(100% - 67px);
|
||||||
|
}
|
||||||
|
#side-panel {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
padding-bottom: 24px;
|
||||||
|
background: var(--background);
|
||||||
|
border-left: var(--border-light);
|
||||||
|
width: 320px;
|
||||||
|
max-width: calc(100vw - 48px - 48px);
|
||||||
|
transform: translateX(100%);
|
||||||
|
transition: transform 130ms ease-in-out;
|
||||||
|
height: calc(100% - 24px);
|
||||||
|
overflow-y: hidden;
|
||||||
|
overflow-x: hidden;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
#side-panel.visible {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#side-panel.wide {
|
||||||
|
width: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#side-panel :global(textarea) {
|
||||||
|
min-height: 100% !important;
|
||||||
|
background-color: var(
|
||||||
|
--spectrum-textfield-m-background-color,
|
||||||
|
var(--spectrum-global-color-gray-50)
|
||||||
|
);
|
||||||
|
padding-top: var(--spacing-l);
|
||||||
|
padding-left: var(--spacing-l);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: var(--spacing-l);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-panel-icons {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-l);
|
||||||
|
}
|
||||||
|
|
||||||
|
.select {
|
||||||
|
flex-basis: calc(33.33% - 10px);
|
||||||
|
width: 0;
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-picker {
|
||||||
|
flex-basis: calc(70% - 32px);
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.freeSearch {
|
||||||
|
flex-basis: 25%;
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -185,7 +185,7 @@
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
{#if !$licensing.backupsEnabled}
|
{#if !$licensing.backupsEnabled}
|
||||||
{#if !$auth.accountPortalAccess && !$licensing.groupsEnabled && $admin.cloud}
|
{#if !$auth.accountPortalAccess && $admin.cloud}
|
||||||
<Body>Contact your account holder to upgrade your plan.</Body>
|
<Body>Contact your account holder to upgrade your plan.</Body>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="pro-buttons">
|
<div class="pro-buttons">
|
||||||
|
|
|
@ -1,21 +1,17 @@
|
||||||
<script>
|
<script>
|
||||||
import {
|
import {
|
||||||
Layout,
|
Layout,
|
||||||
Heading,
|
|
||||||
Body,
|
|
||||||
Button,
|
Button,
|
||||||
Divider,
|
|
||||||
Modal,
|
Modal,
|
||||||
Table,
|
Table,
|
||||||
Tags,
|
|
||||||
Tag,
|
|
||||||
InlineAlert,
|
InlineAlert,
|
||||||
notifications,
|
notifications,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { environment, licensing, auth, admin } from "stores/portal"
|
import { environment, licensing } from "stores/portal"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import CreateEditVariableModal from "components/portal/environment/CreateEditVariableModal.svelte"
|
import CreateEditVariableModal from "components/portal/environment/CreateEditVariableModal.svelte"
|
||||||
import EditVariableColumn from "./_components/EditVariableColumn.svelte"
|
import EditVariableColumn from "./_components/EditVariableColumn.svelte"
|
||||||
|
import LockedFeature from "../../_components/LockedFeature.svelte"
|
||||||
|
|
||||||
let modal
|
let modal
|
||||||
|
|
||||||
|
@ -61,91 +57,43 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Layout noPadding>
|
<LockedFeature
|
||||||
<Layout gap="XS" noPadding>
|
title={"Environment Variables"}
|
||||||
<div class="title">
|
planType={"Business plan"}
|
||||||
<Heading size="M">Environment Variables</Heading>
|
description={"Add and manage environment variables for development and production"}
|
||||||
{#if !$licensing.environmentVariablesEnabled}
|
enabled={$licensing.environmentVariablesEnabled}
|
||||||
<Tags>
|
upgradeButtonClick={async () => {
|
||||||
<Tag icon="LockClosed">Business plan</Tag>
|
await environment.upgradePanelOpened()
|
||||||
</Tags>
|
$licensing.goToUpgradePage()
|
||||||
{/if}
|
}}
|
||||||
</div>
|
>
|
||||||
<Body
|
{#if noEncryptionKey}
|
||||||
>Add and manage environment variables for development and production</Body
|
<InlineAlert
|
||||||
>
|
message="Your Budibase installation does not have a key for encryption, please update your app service's environment variables to contain an 'ENCRYPTION_KEY' value."
|
||||||
</Layout>
|
header="No encryption key found"
|
||||||
<Divider size="S" />
|
type="error"
|
||||||
|
/>
|
||||||
{#if $licensing.environmentVariablesEnabled}
|
|
||||||
{#if noEncryptionKey}
|
|
||||||
<InlineAlert
|
|
||||||
message="Your Budibase installation does not have a key for encryption, please update your app service's environment variables to contain an 'ENCRYPTION_KEY' value."
|
|
||||||
header="No encryption key found"
|
|
||||||
type="error"
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
<div>
|
|
||||||
<Button on:click={modal.show} cta disabled={noEncryptionKey}
|
|
||||||
>Add Variable</Button
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Layout noPadding>
|
|
||||||
<Table
|
|
||||||
{schema}
|
|
||||||
data={$environment.variables}
|
|
||||||
allowEditColumns={false}
|
|
||||||
allowEditRows={false}
|
|
||||||
allowSelectRows={false}
|
|
||||||
{customRenderers}
|
|
||||||
/>
|
|
||||||
</Layout>
|
|
||||||
{:else}
|
|
||||||
<div class="buttons">
|
|
||||||
<Button
|
|
||||||
primary
|
|
||||||
disabled={!$auth.accountPortalAccess && $admin.cloud}
|
|
||||||
on:click={async () => {
|
|
||||||
await environment.upgradePanelOpened()
|
|
||||||
$licensing.goToUpgradePage()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Upgrade
|
|
||||||
</Button>
|
|
||||||
<!--Show the view plans button-->
|
|
||||||
<Button
|
|
||||||
secondary
|
|
||||||
on:click={() => {
|
|
||||||
window.open("https://budibase.com/pricing/", "_blank")
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
View Plans
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
</Layout>
|
<div>
|
||||||
|
<Button on:click={modal.show} cta disabled={noEncryptionKey}
|
||||||
|
>Add Variable</Button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<Layout noPadding>
|
||||||
|
<Table
|
||||||
|
{schema}
|
||||||
|
data={$environment.variables}
|
||||||
|
allowEditColumns={false}
|
||||||
|
allowEditRows={false}
|
||||||
|
allowSelectRows={false}
|
||||||
|
{customRenderers}
|
||||||
|
/>
|
||||||
|
</Layout>
|
||||||
|
</LockedFeature>
|
||||||
|
|
||||||
<Modal bind:this={modal}>
|
<Modal bind:this={modal}>
|
||||||
<CreateEditVariableModal {save} />
|
<CreateEditVariableModal {save} />
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.buttons {
|
|
||||||
display: flex;
|
|
||||||
gap: var(--spacing-l);
|
|
||||||
}
|
|
||||||
.title {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: flex-start;
|
|
||||||
gap: var(--spacing-m);
|
|
||||||
}
|
|
||||||
|
|
||||||
.buttons {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: var(--spacing-m);
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { writable, get } from "svelte/store"
|
||||||
|
import { API } from "api"
|
||||||
|
import { licensing } from "stores/portal"
|
||||||
|
|
||||||
|
export function createAuditLogsStore() {
|
||||||
|
const { subscribe, update } = writable({
|
||||||
|
events: {},
|
||||||
|
logs: {},
|
||||||
|
})
|
||||||
|
|
||||||
|
async function search(opts = {}) {
|
||||||
|
if (get(licensing).auditLogsEnabled) {
|
||||||
|
const paged = await API.searchAuditLogs(opts)
|
||||||
|
|
||||||
|
update(state => {
|
||||||
|
return { ...state, logs: { ...paged, opts } }
|
||||||
|
})
|
||||||
|
|
||||||
|
return paged
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getEventDefinitions() {
|
||||||
|
const events = await API.getEventDefinitions()
|
||||||
|
|
||||||
|
update(state => {
|
||||||
|
return { ...state, ...events }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDownloadUrl(opts = {}) {
|
||||||
|
return API.getDownloadUrl(opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe,
|
||||||
|
search,
|
||||||
|
getEventDefinitions,
|
||||||
|
getDownloadUrl,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const auditLogs = createAuditLogsStore()
|
|
@ -13,3 +13,4 @@ export { backups } from "./backups"
|
||||||
export { overview } from "./overview"
|
export { overview } from "./overview"
|
||||||
export { environment } from "./environment"
|
export { environment } from "./environment"
|
||||||
export { menu } from "./menu"
|
export { menu } from "./menu"
|
||||||
|
export { auditLogs } from "./auditLogs"
|
||||||
|
|
|
@ -67,6 +67,9 @@ export const createLicensingStore = () => {
|
||||||
Constants.Features.ENFORCEABLE_SSO
|
Constants.Features.ENFORCEABLE_SSO
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const auditLogsEnabled = license.features.includes(
|
||||||
|
Constants.Features.AUDIT_LOGS
|
||||||
|
)
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
@ -75,6 +78,7 @@ export const createLicensingStore = () => {
|
||||||
groupsEnabled,
|
groupsEnabled,
|
||||||
backupsEnabled,
|
backupsEnabled,
|
||||||
environmentVariablesEnabled,
|
environmentVariablesEnabled,
|
||||||
|
auditLogsEnabled,
|
||||||
enforceableSSO,
|
enforceableSSO,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -75,6 +75,10 @@ export const menu = derived([admin, auth], ([$admin, $auth]) => {
|
||||||
title: "Usage",
|
title: "Usage",
|
||||||
href: "/builder/portal/account/usage",
|
href: "/builder/portal/account/usage",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "Audit Logs",
|
||||||
|
href: "/builder/portal/account/auditLogs",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
if ($admin.cloud && $auth?.user?.accountPortalAccess) {
|
if ($admin.cloud && $auth?.user?.accountPortalAccess) {
|
||||||
accountSubPages.push({
|
accountSubPages.push({
|
||||||
|
@ -87,6 +91,7 @@ export const menu = derived([admin, auth], ([$admin, $auth]) => {
|
||||||
href: "/builder/portal/account/upgrade",
|
href: "/builder/portal/account/upgrade",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
// add license check here
|
||||||
if (
|
if (
|
||||||
$auth?.user?.accountPortalAccess &&
|
$auth?.user?.accountPortalAccess &&
|
||||||
$auth.user.account.stripeCustomerId
|
$auth.user.account.stripeCustomerId
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
const buildOpts = ({
|
||||||
|
bookmark,
|
||||||
|
userIds,
|
||||||
|
appIds,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
fullSearch,
|
||||||
|
events,
|
||||||
|
}) => {
|
||||||
|
const opts = {}
|
||||||
|
|
||||||
|
if (bookmark) {
|
||||||
|
opts.bookmark = bookmark
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startDate && endDate) {
|
||||||
|
opts.startDate = startDate
|
||||||
|
opts.endDate = endDate
|
||||||
|
} else if (startDate && !endDate) {
|
||||||
|
opts.startDate = startDate
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fullSearch) {
|
||||||
|
opts.fullSearch = fullSearch
|
||||||
|
}
|
||||||
|
|
||||||
|
if (events.length) {
|
||||||
|
opts.events = events
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userIds.length) {
|
||||||
|
opts.userIds = userIds
|
||||||
|
}
|
||||||
|
|
||||||
|
if (appIds.length) {
|
||||||
|
opts.appIds = appIds
|
||||||
|
}
|
||||||
|
|
||||||
|
return opts
|
||||||
|
}
|
||||||
|
|
||||||
|
export const buildAuditLogsEndpoints = API => ({
|
||||||
|
/**
|
||||||
|
* Gets a list of users in the current tenant.
|
||||||
|
*/
|
||||||
|
searchAuditLogs: async opts => {
|
||||||
|
return await API.post({
|
||||||
|
url: `/api/global/auditlogs/search`,
|
||||||
|
body: buildOpts(opts),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
getEventDefinitions: async () => {
|
||||||
|
return await API.get({
|
||||||
|
url: `/api/global/auditlogs/definitions`,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
getDownloadUrl: opts => {
|
||||||
|
const query = encodeURIComponent(JSON.stringify(opts))
|
||||||
|
return `/api/global/auditlogs/download?query=${query}`
|
||||||
|
},
|
||||||
|
})
|
|
@ -28,6 +28,8 @@ import { buildPluginEndpoints } from "./plugins"
|
||||||
import { buildBackupsEndpoints } from "./backups"
|
import { buildBackupsEndpoints } from "./backups"
|
||||||
import { buildEnvironmentVariableEndpoints } from "./environmentVariables"
|
import { buildEnvironmentVariableEndpoints } from "./environmentVariables"
|
||||||
import { buildEventEndpoints } from "./events"
|
import { buildEventEndpoints } from "./events"
|
||||||
|
import { buildAuditLogsEndpoints } from "./auditLogs"
|
||||||
|
|
||||||
const defaultAPIClientConfig = {
|
const defaultAPIClientConfig = {
|
||||||
/**
|
/**
|
||||||
* Certain definitions can't change at runtime for client apps, such as the
|
* Certain definitions can't change at runtime for client apps, such as the
|
||||||
|
@ -250,5 +252,6 @@ export const createAPIClient = config => {
|
||||||
...buildBackupsEndpoints(API),
|
...buildBackupsEndpoints(API),
|
||||||
...buildEnvironmentVariableEndpoints(API),
|
...buildEnvironmentVariableEndpoints(API),
|
||||||
...buildEventEndpoints(API),
|
...buildEventEndpoints(API),
|
||||||
|
...buildAuditLogsEndpoints(API),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -115,6 +115,7 @@ export const Features = {
|
||||||
USER_GROUPS: "userGroups",
|
USER_GROUPS: "userGroups",
|
||||||
BACKUPS: "appBackups",
|
BACKUPS: "appBackups",
|
||||||
ENVIRONMENT_VARIABLES: "environmentVariables",
|
ENVIRONMENT_VARIABLES: "environmentVariables",
|
||||||
|
AUDIT_LOGS: "auditLogs",
|
||||||
ENFORCEABLE_SSO: "enforceableSSO",
|
ENFORCEABLE_SSO: "enforceableSSO",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -39,6 +39,7 @@ const config: Config.InitialOptions = {
|
||||||
],
|
],
|
||||||
collectCoverageFrom: [
|
collectCoverageFrom: [
|
||||||
"src/**/*.{js,ts}",
|
"src/**/*.{js,ts}",
|
||||||
|
"../backend-core/src/**/*.{js,ts}",
|
||||||
// The use of coverage with couchdb view functions breaks tests
|
// The use of coverage with couchdb view functions breaks tests
|
||||||
"!src/db/views/staticViews.*",
|
"!src/db/views/staticViews.*",
|
||||||
],
|
],
|
||||||
|
|
|
@ -87,6 +87,7 @@
|
||||||
"koa-send": "5.0.0",
|
"koa-send": "5.0.0",
|
||||||
"koa-session": "5.12.0",
|
"koa-session": "5.12.0",
|
||||||
"koa-static": "5.0.0",
|
"koa-static": "5.0.0",
|
||||||
|
"koa-useragent": "^4.1.0",
|
||||||
"koa2-ratelimit": "1.1.1",
|
"koa2-ratelimit": "1.1.1",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
"memorystream": "0.3.1",
|
"memorystream": "0.3.1",
|
||||||
|
|
|
@ -24,8 +24,7 @@ import { breakExternalTableId, isSQL } from "../../../integrations/utils"
|
||||||
import { processObjectSync } from "@budibase/string-templates"
|
import { processObjectSync } from "@budibase/string-templates"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
import { processFormulas, processDates } from "../../../utilities/rowProcessor"
|
import { processFormulas, processDates } from "../../../utilities/rowProcessor"
|
||||||
import { context } from "@budibase/backend-core"
|
import { db as dbCore } from "@budibase/backend-core"
|
||||||
import { removeKeyNumbering } from "./utils"
|
|
||||||
import sdk from "../../../sdk"
|
import sdk from "../../../sdk"
|
||||||
|
|
||||||
export interface ManyRelationship {
|
export interface ManyRelationship {
|
||||||
|
@ -61,7 +60,7 @@ function buildFilters(
|
||||||
let prefix = 1
|
let prefix = 1
|
||||||
for (let operator of Object.values(filters)) {
|
for (let operator of Object.values(filters)) {
|
||||||
for (let field of Object.keys(operator || {})) {
|
for (let field of Object.keys(operator || {})) {
|
||||||
if (removeKeyNumbering(field) === "_id") {
|
if (dbCore.removeKeyNumbering(field) === "_id") {
|
||||||
if (primary) {
|
if (primary) {
|
||||||
const parts = breakRowIdField(operator[field])
|
const parts = breakRowIdField(operator[field])
|
||||||
for (let field of primary) {
|
for (let field of primary) {
|
||||||
|
|
|
@ -1,531 +1,18 @@
|
||||||
import { SearchIndexes } from "../../../db/utils"
|
import { db as dbCore, context, SearchParams } from "@budibase/backend-core"
|
||||||
import { removeKeyNumbering } from "./utils"
|
import { SearchFilters, Row, SearchIndex } from "@budibase/types"
|
||||||
import fetch from "node-fetch"
|
|
||||||
import { db as dbCore, context } from "@budibase/backend-core"
|
|
||||||
import { SearchFilters, Row } from "@budibase/types"
|
|
||||||
|
|
||||||
type SearchParams = {
|
|
||||||
tableId: string
|
|
||||||
sort?: string
|
|
||||||
sortOrder?: string
|
|
||||||
sortType?: string
|
|
||||||
limit?: number
|
|
||||||
bookmark?: string
|
|
||||||
version?: string
|
|
||||||
rows?: Row[]
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class to build lucene query URLs.
|
|
||||||
* Optionally takes a base lucene query object.
|
|
||||||
*/
|
|
||||||
export class QueryBuilder {
|
|
||||||
query: SearchFilters
|
|
||||||
limit: number
|
|
||||||
sort?: string
|
|
||||||
bookmark?: string
|
|
||||||
sortOrder: string
|
|
||||||
sortType: string
|
|
||||||
includeDocs: boolean
|
|
||||||
version?: string
|
|
||||||
|
|
||||||
constructor(base?: SearchFilters) {
|
|
||||||
this.query = {
|
|
||||||
allOr: false,
|
|
||||||
string: {},
|
|
||||||
fuzzy: {},
|
|
||||||
range: {},
|
|
||||||
equal: {},
|
|
||||||
notEqual: {},
|
|
||||||
empty: {},
|
|
||||||
notEmpty: {},
|
|
||||||
oneOf: {},
|
|
||||||
contains: {},
|
|
||||||
notContains: {},
|
|
||||||
containsAny: {},
|
|
||||||
...base,
|
|
||||||
}
|
|
||||||
this.limit = 50
|
|
||||||
this.sortOrder = "ascending"
|
|
||||||
this.sortType = "string"
|
|
||||||
this.includeDocs = true
|
|
||||||
}
|
|
||||||
|
|
||||||
setVersion(version?: string) {
|
|
||||||
if (version != null) {
|
|
||||||
this.version = version
|
|
||||||
}
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
setTable(tableId: string) {
|
|
||||||
this.query.equal!.tableId = tableId
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
setLimit(limit?: number) {
|
|
||||||
if (limit != null) {
|
|
||||||
this.limit = limit
|
|
||||||
}
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
setSort(sort?: string) {
|
|
||||||
if (sort != null) {
|
|
||||||
this.sort = sort
|
|
||||||
}
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
setSortOrder(sortOrder?: string) {
|
|
||||||
if (sortOrder != null) {
|
|
||||||
this.sortOrder = sortOrder
|
|
||||||
}
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
setSortType(sortType?: string) {
|
|
||||||
if (sortType != null) {
|
|
||||||
this.sortType = sortType
|
|
||||||
}
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
setBookmark(bookmark?: string) {
|
|
||||||
if (bookmark != null) {
|
|
||||||
this.bookmark = bookmark
|
|
||||||
}
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
excludeDocs() {
|
|
||||||
this.includeDocs = false
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
addString(key: string, partial: string) {
|
|
||||||
this.query.string![key] = partial
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
addFuzzy(key: string, fuzzy: string) {
|
|
||||||
this.query.fuzzy![key] = fuzzy
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
addRange(key: string, low: string | number, high: string | number) {
|
|
||||||
this.query.range![key] = {
|
|
||||||
low,
|
|
||||||
high,
|
|
||||||
}
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
addEqual(key: string, value: any) {
|
|
||||||
this.query.equal![key] = value
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
addNotEqual(key: string, value: any) {
|
|
||||||
this.query.notEqual![key] = value
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
addEmpty(key: string, value: any) {
|
|
||||||
this.query.empty![key] = value
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
addNotEmpty(key: string, value: any) {
|
|
||||||
this.query.notEmpty![key] = value
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
addOneOf(key: string, value: any) {
|
|
||||||
this.query.oneOf![key] = value
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
addContains(key: string, value: any) {
|
|
||||||
this.query.contains![key] = value
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
addNotContains(key: string, value: any) {
|
|
||||||
this.query.notContains![key] = value
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
addContainsAny(key: string, value: any) {
|
|
||||||
this.query.containsAny![key] = value
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Preprocesses a value before going into a lucene search.
|
|
||||||
* Transforms strings to lowercase and wraps strings and bools in quotes.
|
|
||||||
* @param value The value to process
|
|
||||||
* @param options The preprocess options
|
|
||||||
* @returns {string|*}
|
|
||||||
*/
|
|
||||||
preprocess(value: any, { escape, lowercase, wrap, type }: any = {}) {
|
|
||||||
const hasVersion = !!this.version
|
|
||||||
// Determine if type needs wrapped
|
|
||||||
const originalType = typeof value
|
|
||||||
// Convert to lowercase
|
|
||||||
if (value && lowercase) {
|
|
||||||
value = value.toLowerCase ? value.toLowerCase() : value
|
|
||||||
}
|
|
||||||
// Escape characters
|
|
||||||
if (escape && originalType === "string") {
|
|
||||||
value = `${value}`.replace(/[ #+\-&|!(){}\]^"~*?:\\]/g, "\\$&")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wrap in quotes
|
|
||||||
if (originalType === "string" && !isNaN(value) && !type) {
|
|
||||||
value = `"${value}"`
|
|
||||||
} else if (hasVersion && wrap) {
|
|
||||||
value = originalType === "number" ? value : `"${value}"`
|
|
||||||
}
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
buildSearchQuery() {
|
|
||||||
const builder = this
|
|
||||||
let allOr = this.query && this.query.allOr
|
|
||||||
let query = allOr ? "" : "*:*"
|
|
||||||
const allPreProcessingOpts = { escape: true, lowercase: true, wrap: true }
|
|
||||||
let tableId
|
|
||||||
if (this.query.equal!.tableId) {
|
|
||||||
tableId = this.query.equal!.tableId
|
|
||||||
delete this.query.equal!.tableId
|
|
||||||
}
|
|
||||||
|
|
||||||
const equal = (key: string, value: any) => {
|
|
||||||
// 0 evaluates to false, which means we would return all rows if we don't check it
|
|
||||||
if (!value && value !== 0) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return `${key}:${builder.preprocess(value, allPreProcessingOpts)}`
|
|
||||||
}
|
|
||||||
|
|
||||||
const contains = (key: string, value: any, mode = "AND") => {
|
|
||||||
if (Array.isArray(value) && value.length === 0) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
if (!Array.isArray(value)) {
|
|
||||||
return `${key}:${value}`
|
|
||||||
}
|
|
||||||
let statement = `${builder.preprocess(value[0], { escape: true })}`
|
|
||||||
for (let i = 1; i < value.length; i++) {
|
|
||||||
statement += ` ${mode} ${builder.preprocess(value[i], {
|
|
||||||
escape: true,
|
|
||||||
})}`
|
|
||||||
}
|
|
||||||
return `${key}:(${statement})`
|
|
||||||
}
|
|
||||||
|
|
||||||
const notContains = (key: string, value: any) => {
|
|
||||||
// @ts-ignore
|
|
||||||
const allPrefix = allOr === "" ? "*:* AND" : ""
|
|
||||||
return allPrefix + "NOT " + contains(key, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
const containsAny = (key: string, value: any) => {
|
|
||||||
return contains(key, value, "OR")
|
|
||||||
}
|
|
||||||
|
|
||||||
const oneOf = (key: string, value: any) => {
|
|
||||||
if (!Array.isArray(value)) {
|
|
||||||
if (typeof value === "string") {
|
|
||||||
value = value.split(",")
|
|
||||||
} else {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let orStatement = `${builder.preprocess(value[0], allPreProcessingOpts)}`
|
|
||||||
for (let i = 1; i < value.length; i++) {
|
|
||||||
orStatement += ` OR ${builder.preprocess(
|
|
||||||
value[i],
|
|
||||||
allPreProcessingOpts
|
|
||||||
)}`
|
|
||||||
}
|
|
||||||
return `${key}:(${orStatement})`
|
|
||||||
}
|
|
||||||
|
|
||||||
function build(structure: any, queryFn: any) {
|
|
||||||
for (let [key, value] of Object.entries(structure)) {
|
|
||||||
// check for new format - remove numbering if needed
|
|
||||||
key = removeKeyNumbering(key)
|
|
||||||
key = builder.preprocess(key.replace(/ /g, "_"), {
|
|
||||||
escape: true,
|
|
||||||
})
|
|
||||||
const expression = queryFn(key, value)
|
|
||||||
if (expression == null) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (query.length > 0) {
|
|
||||||
query += ` ${allOr ? "OR" : "AND"} `
|
|
||||||
}
|
|
||||||
query += expression
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Construct the actual lucene search query string from JSON structure
|
|
||||||
if (this.query.string) {
|
|
||||||
build(this.query.string, (key: string, value: any) => {
|
|
||||||
if (!value) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
value = builder.preprocess(value, {
|
|
||||||
escape: true,
|
|
||||||
lowercase: true,
|
|
||||||
type: "string",
|
|
||||||
})
|
|
||||||
return `${key}:${value}*`
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (this.query.range) {
|
|
||||||
build(this.query.range, (key: string, value: any) => {
|
|
||||||
if (!value) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
if (value.low == null || value.low === "") {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
if (value.high == null || value.high === "") {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
const low = builder.preprocess(value.low, allPreProcessingOpts)
|
|
||||||
const high = builder.preprocess(value.high, allPreProcessingOpts)
|
|
||||||
return `${key}:[${low} TO ${high}]`
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (this.query.fuzzy) {
|
|
||||||
build(this.query.fuzzy, (key: string, value: any) => {
|
|
||||||
if (!value) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
value = builder.preprocess(value, {
|
|
||||||
escape: true,
|
|
||||||
lowercase: true,
|
|
||||||
type: "fuzzy",
|
|
||||||
})
|
|
||||||
return `${key}:${value}~`
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (this.query.equal) {
|
|
||||||
build(this.query.equal, equal)
|
|
||||||
}
|
|
||||||
if (this.query.notEqual) {
|
|
||||||
build(this.query.notEqual, (key: string, value: any) => {
|
|
||||||
if (!value) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return `!${key}:${builder.preprocess(value, allPreProcessingOpts)}`
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (this.query.empty) {
|
|
||||||
build(this.query.empty, (key: string) => `!${key}:["" TO *]`)
|
|
||||||
}
|
|
||||||
if (this.query.notEmpty) {
|
|
||||||
build(this.query.notEmpty, (key: string) => `${key}:["" TO *]`)
|
|
||||||
}
|
|
||||||
if (this.query.oneOf) {
|
|
||||||
build(this.query.oneOf, oneOf)
|
|
||||||
}
|
|
||||||
if (this.query.contains) {
|
|
||||||
build(this.query.contains, contains)
|
|
||||||
}
|
|
||||||
if (this.query.notContains) {
|
|
||||||
build(this.query.notContains, notContains)
|
|
||||||
}
|
|
||||||
if (this.query.containsAny) {
|
|
||||||
build(this.query.containsAny, containsAny)
|
|
||||||
}
|
|
||||||
// make sure table ID is always added as an AND
|
|
||||||
if (tableId) {
|
|
||||||
query = `(${query})`
|
|
||||||
allOr = false
|
|
||||||
build({ tableId }, equal)
|
|
||||||
}
|
|
||||||
return query
|
|
||||||
}
|
|
||||||
|
|
||||||
buildSearchBody() {
|
|
||||||
let body: any = {
|
|
||||||
q: this.buildSearchQuery(),
|
|
||||||
limit: Math.min(this.limit, 200),
|
|
||||||
include_docs: this.includeDocs,
|
|
||||||
}
|
|
||||||
if (this.bookmark) {
|
|
||||||
body.bookmark = this.bookmark
|
|
||||||
}
|
|
||||||
if (this.sort) {
|
|
||||||
const order = this.sortOrder === "descending" ? "-" : ""
|
|
||||||
const type = `<${this.sortType}>`
|
|
||||||
body.sort = `${order}${this.sort.replace(/ /g, "_")}${type}`
|
|
||||||
}
|
|
||||||
return body
|
|
||||||
}
|
|
||||||
|
|
||||||
async run() {
|
|
||||||
const appId = context.getAppId()
|
|
||||||
const { url, cookie } = dbCore.getCouchInfo()
|
|
||||||
const fullPath = `${url}/${appId}/_design/database/_search/${SearchIndexes.ROWS}`
|
|
||||||
const body = this.buildSearchBody()
|
|
||||||
return await runQuery(fullPath, body, cookie)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Executes a lucene search query.
|
|
||||||
* @param url The query URL
|
|
||||||
* @param body The request body defining search criteria
|
|
||||||
* @param cookie The auth cookie for CouchDB
|
|
||||||
* @returns {Promise<{rows: []}>}
|
|
||||||
*/
|
|
||||||
const runQuery = async (url: string, body: any, cookie: string) => {
|
|
||||||
const response = await fetch(url, {
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
Authorization: cookie,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
const json = await response.json()
|
|
||||||
|
|
||||||
let output: any = {
|
|
||||||
rows: [],
|
|
||||||
}
|
|
||||||
if (json.rows != null && json.rows.length > 0) {
|
|
||||||
output.rows = json.rows.map((row: any) => row.doc)
|
|
||||||
}
|
|
||||||
if (json.bookmark) {
|
|
||||||
output.bookmark = json.bookmark
|
|
||||||
}
|
|
||||||
return output
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets round the fixed limit of 200 results from a query by fetching as many
|
|
||||||
* pages as required and concatenating the results. This recursively operates
|
|
||||||
* until enough results have been found.
|
|
||||||
* @param query {object} The JSON query structure
|
|
||||||
* @param params {object} The search params including:
|
|
||||||
* tableId {string} The table ID to search
|
|
||||||
* sort {string} The sort column
|
|
||||||
* sortOrder {string} The sort order ("ascending" or "descending")
|
|
||||||
* sortType {string} Whether to treat sortable values as strings or
|
|
||||||
* numbers. ("string" or "number")
|
|
||||||
* limit {number} The number of results to fetch
|
|
||||||
* bookmark {string|null} Current bookmark in the recursive search
|
|
||||||
* rows {array|null} Current results in the recursive search
|
|
||||||
* @returns {Promise<*[]|*>}
|
|
||||||
*/
|
|
||||||
async function recursiveSearch(query: any, params: any): Promise<any> {
|
|
||||||
const bookmark = params.bookmark
|
|
||||||
const rows = params.rows || []
|
|
||||||
if (rows.length >= params.limit) {
|
|
||||||
return rows
|
|
||||||
}
|
|
||||||
let pageSize = 200
|
|
||||||
if (rows.length > params.limit - 200) {
|
|
||||||
pageSize = params.limit - rows.length
|
|
||||||
}
|
|
||||||
const page = await new QueryBuilder(query)
|
|
||||||
.setVersion(params.version)
|
|
||||||
.setTable(params.tableId)
|
|
||||||
.setBookmark(bookmark)
|
|
||||||
.setLimit(pageSize)
|
|
||||||
.setSort(params.sort)
|
|
||||||
.setSortOrder(params.sortOrder)
|
|
||||||
.setSortType(params.sortType)
|
|
||||||
.run()
|
|
||||||
if (!page.rows.length) {
|
|
||||||
return rows
|
|
||||||
}
|
|
||||||
if (page.rows.length < 200) {
|
|
||||||
return [...rows, ...page.rows]
|
|
||||||
}
|
|
||||||
const newParams = {
|
|
||||||
...params,
|
|
||||||
bookmark: page.bookmark,
|
|
||||||
rows: [...rows, ...page.rows],
|
|
||||||
}
|
|
||||||
return await recursiveSearch(query, newParams)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Performs a paginated search. A bookmark will be returned to allow the next
|
|
||||||
* page to be fetched. There is a max limit off 200 results per page in a
|
|
||||||
* paginated search.
|
|
||||||
* @param query {object} The JSON query structure
|
|
||||||
* @param params {object} The search params including:
|
|
||||||
* tableId {string} The table ID to search
|
|
||||||
* sort {string} The sort column
|
|
||||||
* sortOrder {string} The sort order ("ascending" or "descending")
|
|
||||||
* sortType {string} Whether to treat sortable values as strings or
|
|
||||||
* numbers. ("string" or "number")
|
|
||||||
* limit {number} The desired page size
|
|
||||||
* bookmark {string} The bookmark to resume from
|
|
||||||
* @returns {Promise<{hasNextPage: boolean, rows: *[]}>}
|
|
||||||
*/
|
|
||||||
export async function paginatedSearch(
|
export async function paginatedSearch(
|
||||||
query: SearchFilters,
|
query: SearchFilters,
|
||||||
params: SearchParams
|
params: SearchParams<Row>
|
||||||
) {
|
) {
|
||||||
let limit = params.limit
|
const appId = context.getAppId()
|
||||||
if (limit == null || isNaN(limit) || limit < 0) {
|
return dbCore.paginatedSearch(appId!, SearchIndex.ROWS, query, params)
|
||||||
limit = 50
|
|
||||||
}
|
|
||||||
limit = Math.min(limit, 200)
|
|
||||||
const search = new QueryBuilder(query)
|
|
||||||
.setVersion(params.version)
|
|
||||||
.setTable(params.tableId)
|
|
||||||
.setSort(params.sort)
|
|
||||||
.setSortOrder(params.sortOrder)
|
|
||||||
.setSortType(params.sortType)
|
|
||||||
const searchResults = await search
|
|
||||||
.setBookmark(params.bookmark)
|
|
||||||
.setLimit(limit)
|
|
||||||
.run()
|
|
||||||
|
|
||||||
// Try fetching 1 row in the next page to see if another page of results
|
|
||||||
// exists or not
|
|
||||||
const nextResults = await search
|
|
||||||
.setTable(params.tableId)
|
|
||||||
.setBookmark(searchResults.bookmark)
|
|
||||||
.setLimit(1)
|
|
||||||
.run()
|
|
||||||
|
|
||||||
return {
|
|
||||||
...searchResults,
|
|
||||||
hasNextPage: nextResults.rows && nextResults.rows.length > 0,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export async function fullSearch(
|
||||||
* Performs a full search, fetching multiple pages if required to return the
|
query: SearchFilters,
|
||||||
* desired amount of results. There is a limit of 1000 results to avoid
|
params: SearchParams<Row>
|
||||||
* heavy performance hits, and to avoid client components breaking from
|
) {
|
||||||
* handling too much data.
|
const appId = context.getAppId()
|
||||||
* @param query {object} The JSON query structure
|
return dbCore.fullSearch(appId!, SearchIndex.ROWS, query, params)
|
||||||
* @param params {object} The search params including:
|
|
||||||
* tableId {string} The table ID to search
|
|
||||||
* sort {string} The sort column
|
|
||||||
* sortOrder {string} The sort order ("ascending" or "descending")
|
|
||||||
* sortType {string} Whether to treat sortable values as strings or
|
|
||||||
* numbers. ("string" or "number")
|
|
||||||
* limit {number} The desired number of results
|
|
||||||
* @returns {Promise<{rows: *}>}
|
|
||||||
*/
|
|
||||||
export async function fullSearch(query: SearchFilters, params: SearchParams) {
|
|
||||||
let limit = params.limit
|
|
||||||
if (limit == null || isNaN(limit) || limit < 0) {
|
|
||||||
limit = 1000
|
|
||||||
}
|
|
||||||
params.limit = Math.min(limit, 1000)
|
|
||||||
const rows = await recursiveSearch(query, params)
|
|
||||||
return { rows }
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,8 +3,7 @@ import * as userController from "../user"
|
||||||
import { FieldTypes } from "../../../constants"
|
import { FieldTypes } from "../../../constants"
|
||||||
import { context } from "@budibase/backend-core"
|
import { context } from "@budibase/backend-core"
|
||||||
import { makeExternalQuery } from "../../../integrations/base/query"
|
import { makeExternalQuery } from "../../../integrations/base/query"
|
||||||
import { BBContext, Row, Table } from "@budibase/types"
|
import { Row, Table } from "@budibase/types"
|
||||||
export { removeKeyNumbering } from "../../../integrations/base/utils"
|
|
||||||
const validateJs = require("validate.js")
|
const validateJs = require("validate.js")
|
||||||
const { cloneDeep } = require("lodash/fp")
|
const { cloneDeep } = require("lodash/fp")
|
||||||
import { Format } from "../view/exporters"
|
import { Format } from "../view/exporters"
|
||||||
|
|
|
@ -32,6 +32,7 @@ import { initialise as initialiseWebsockets } from "./websocket"
|
||||||
import { startup } from "./startup"
|
import { startup } from "./startup"
|
||||||
const Sentry = require("@sentry/node")
|
const Sentry = require("@sentry/node")
|
||||||
const destroyable = require("server-destroy")
|
const destroyable = require("server-destroy")
|
||||||
|
const { userAgent } = require("koa-useragent")
|
||||||
|
|
||||||
const app = new Koa()
|
const app = new Koa()
|
||||||
|
|
||||||
|
@ -53,6 +54,7 @@ app.use(
|
||||||
)
|
)
|
||||||
|
|
||||||
app.use(middleware.logging)
|
app.use(middleware.logging)
|
||||||
|
app.use(userAgent)
|
||||||
|
|
||||||
if (env.isProd()) {
|
if (env.isProd()) {
|
||||||
env._set("NODE_ENV", "production")
|
env._set("NODE_ENV", "production")
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { init as coreInit } from "@budibase/backend-core"
|
import * as core from "@budibase/backend-core"
|
||||||
import env from "../environment"
|
import env from "../environment"
|
||||||
|
|
||||||
export function init() {
|
export function init() {
|
||||||
|
@ -12,5 +12,5 @@ export function init() {
|
||||||
dbConfig.allDbs = true
|
dbConfig.allDbs = true
|
||||||
}
|
}
|
||||||
|
|
||||||
coreInit({ db: dbConfig })
|
core.init({ db: dbConfig })
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,10 +9,6 @@ export const AppStatus = {
|
||||||
DEPLOYED: "published",
|
DEPLOYED: "published",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SearchIndexes = {
|
|
||||||
ROWS: "rows",
|
|
||||||
}
|
|
||||||
|
|
||||||
export const BudibaseInternalDB = {
|
export const BudibaseInternalDB = {
|
||||||
_id: "bb_internal",
|
_id: "bb_internal",
|
||||||
type: dbCore.BUDIBASE_DATASOURCE_TYPE,
|
type: dbCore.BUDIBASE_DATASOURCE_TYPE,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { context } from "@budibase/backend-core"
|
import { context } from "@budibase/backend-core"
|
||||||
import { DocumentType, SEPARATOR, ViewName, SearchIndexes } from "../utils"
|
import { DocumentType, SEPARATOR, ViewName } from "../utils"
|
||||||
import { LinkDocument, Row } from "@budibase/types"
|
import { LinkDocument, Row, SearchIndex } from "@budibase/types"
|
||||||
const SCREEN_PREFIX = DocumentType.SCREEN + SEPARATOR
|
const SCREEN_PREFIX = DocumentType.SCREEN + SEPARATOR
|
||||||
|
|
||||||
/**************************************************
|
/**************************************************
|
||||||
|
@ -91,7 +91,7 @@ async function searchIndex(indexName: string, fnString: string) {
|
||||||
|
|
||||||
export async function createAllSearchIndex() {
|
export async function createAllSearchIndex() {
|
||||||
await searchIndex(
|
await searchIndex(
|
||||||
SearchIndexes.ROWS,
|
SearchIndex.ROWS,
|
||||||
function (doc: Row) {
|
function (doc: Row) {
|
||||||
function idx(input: Row, prev?: string) {
|
function idx(input: Row, prev?: string) {
|
||||||
for (let key of Object.keys(input)) {
|
for (let key of Object.keys(input)) {
|
||||||
|
|
|
@ -6,11 +6,11 @@ import {
|
||||||
SearchFilters,
|
SearchFilters,
|
||||||
SortDirection,
|
SortDirection,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
|
import { db as dbCore } from "@budibase/backend-core"
|
||||||
import { QueryOptions } from "../../definitions/datasource"
|
import { QueryOptions } from "../../definitions/datasource"
|
||||||
import { isIsoDateString, SqlClient } from "../utils"
|
import { isIsoDateString, SqlClient } from "../utils"
|
||||||
import SqlTableQueryBuilder from "./sqlTable"
|
import SqlTableQueryBuilder from "./sqlTable"
|
||||||
import environment from "../../environment"
|
import environment from "../../environment"
|
||||||
import { removeKeyNumbering } from "./utils"
|
|
||||||
|
|
||||||
const envLimit = environment.SQL_MAX_ROWS
|
const envLimit = environment.SQL_MAX_ROWS
|
||||||
? parseInt(environment.SQL_MAX_ROWS)
|
? parseInt(environment.SQL_MAX_ROWS)
|
||||||
|
@ -136,7 +136,7 @@ class InternalBuilder {
|
||||||
fn: (key: string, value: any) => void
|
fn: (key: string, value: any) => void
|
||||||
) {
|
) {
|
||||||
for (let [key, value] of Object.entries(structure)) {
|
for (let [key, value] of Object.entries(structure)) {
|
||||||
const updatedKey = removeKeyNumbering(key)
|
const updatedKey = dbCore.removeKeyNumbering(key)
|
||||||
const isRelationshipField = updatedKey.includes(".")
|
const isRelationshipField = updatedKey.includes(".")
|
||||||
if (!opts.relationship && !isRelationshipField) {
|
if (!opts.relationship && !isRelationshipField) {
|
||||||
fn(`${opts.tableName}.${updatedKey}`, value)
|
fn(`${opts.tableName}.${updatedKey}`, value)
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
const QUERY_START_REGEX = /\d[0-9]*:/g
|
|
||||||
|
|
||||||
export function removeKeyNumbering(key: any): string {
|
|
||||||
if (typeof key === "string" && key.match(QUERY_START_REGEX) != null) {
|
|
||||||
const parts = key.split(":")
|
|
||||||
// remove the number
|
|
||||||
parts.shift()
|
|
||||||
return parts.join(":")
|
|
||||||
} else {
|
|
||||||
return key
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -5,7 +5,7 @@ import {
|
||||||
generateApiKey,
|
generateApiKey,
|
||||||
getChecklist,
|
getChecklist,
|
||||||
} from "./utilities/workerRequests"
|
} from "./utilities/workerRequests"
|
||||||
import { installation, tenancy, logging } from "@budibase/backend-core"
|
import { installation, tenancy, logging, events } from "@budibase/backend-core"
|
||||||
import fs from "fs"
|
import fs from "fs"
|
||||||
import { watch } from "./watch"
|
import { watch } from "./watch"
|
||||||
import * as automations from "./automations"
|
import * as automations from "./automations"
|
||||||
|
@ -124,6 +124,9 @@ export async function startup(app?: any, server?: any) {
|
||||||
// get the references to the queue promises, don't await as
|
// get the references to the queue promises, don't await as
|
||||||
// they will never end, unless the processing stops
|
// they will never end, unless the processing stops
|
||||||
let queuePromises = []
|
let queuePromises = []
|
||||||
|
// configure events to use the pro audit log write
|
||||||
|
// can't integrate directly into backend-core due to cyclic issues
|
||||||
|
queuePromises.push(events.processors.init(pro.sdk.auditLogs.write))
|
||||||
queuePromises.push(automations.init())
|
queuePromises.push(automations.init())
|
||||||
queuePromises.push(initPro())
|
queuePromises.push(initPro())
|
||||||
if (app) {
|
if (app) {
|
||||||
|
|
|
@ -6122,20 +6122,13 @@ dir-glob@^3.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
path-type "^4.0.0"
|
path-type "^4.0.0"
|
||||||
|
|
||||||
docker-compose@0.23.17:
|
docker-compose@0.23.17, docker-compose@^0.23.5:
|
||||||
version "0.23.17"
|
version "0.23.17"
|
||||||
resolved "https://registry.yarnpkg.com/docker-compose/-/docker-compose-0.23.17.tgz#8816bef82562d9417dc8c790aa4871350f93a2ba"
|
resolved "https://registry.yarnpkg.com/docker-compose/-/docker-compose-0.23.17.tgz#8816bef82562d9417dc8c790aa4871350f93a2ba"
|
||||||
integrity sha512-YJV18YoYIcxOdJKeFcCFihE6F4M2NExWM/d4S1ITcS9samHKnNUihz9kjggr0dNtsrbpFNc7/Yzd19DWs+m1xg==
|
integrity sha512-YJV18YoYIcxOdJKeFcCFihE6F4M2NExWM/d4S1ITcS9samHKnNUihz9kjggr0dNtsrbpFNc7/Yzd19DWs+m1xg==
|
||||||
dependencies:
|
dependencies:
|
||||||
yaml "^1.10.2"
|
yaml "^1.10.2"
|
||||||
|
|
||||||
docker-compose@^0.23.5:
|
|
||||||
version "0.23.19"
|
|
||||||
resolved "https://registry.yarnpkg.com/docker-compose/-/docker-compose-0.23.19.tgz#9947726e2fe67bdfa9e8efe1ff15aa0de2e10eb8"
|
|
||||||
integrity sha512-v5vNLIdUqwj4my80wxFDkNH+4S85zsRuH29SO7dCWVWPCMt/ohZBsGN6g6KXWifT0pzQ7uOxqEKCYCDPJ8Vz4g==
|
|
||||||
dependencies:
|
|
||||||
yaml "^1.10.2"
|
|
||||||
|
|
||||||
docker-modem@^3.0.0:
|
docker-modem@^3.0.0:
|
||||||
version "3.0.6"
|
version "3.0.6"
|
||||||
resolved "https://registry.yarnpkg.com/docker-modem/-/docker-modem-3.0.6.tgz#8c76338641679e28ec2323abb65b3276fb1ce597"
|
resolved "https://registry.yarnpkg.com/docker-modem/-/docker-modem-3.0.6.tgz#8c76338641679e28ec2323abb65b3276fb1ce597"
|
||||||
|
@ -6983,6 +6976,11 @@ expose-loader@^3.1.0:
|
||||||
resolved "https://registry.yarnpkg.com/expose-loader/-/expose-loader-3.1.0.tgz#7a0bdecb345b921ca238a8c4715a4ea7e227213f"
|
resolved "https://registry.yarnpkg.com/expose-loader/-/expose-loader-3.1.0.tgz#7a0bdecb345b921ca238a8c4715a4ea7e227213f"
|
||||||
integrity sha512-2RExSo0yJiqP+xiUue13jQa2IHE8kLDzTI7b6kn+vUlBVvlzNSiLDzo4e5Pp5J039usvTUnxZ8sUOhv0Kg15NA==
|
integrity sha512-2RExSo0yJiqP+xiUue13jQa2IHE8kLDzTI7b6kn+vUlBVvlzNSiLDzo4e5Pp5J039usvTUnxZ8sUOhv0Kg15NA==
|
||||||
|
|
||||||
|
express-useragent@^1.0.15:
|
||||||
|
version "1.0.15"
|
||||||
|
resolved "https://registry.yarnpkg.com/express-useragent/-/express-useragent-1.0.15.tgz#cefda5fa4904345d51d3368b117a8dd4124985d9"
|
||||||
|
integrity sha512-eq5xMiYCYwFPoekffMjvEIk+NWdlQY9Y38OsTyl13IvA728vKT+q/CSERYWzcw93HGBJcIqMIsZC5CZGARPVdg==
|
||||||
|
|
||||||
ext-list@^2.0.0:
|
ext-list@^2.0.0:
|
||||||
version "2.2.2"
|
version "2.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/ext-list/-/ext-list-2.2.2.tgz#0b98e64ed82f5acf0f2931babf69212ef52ddd37"
|
resolved "https://registry.yarnpkg.com/ext-list/-/ext-list-2.2.2.tgz#0b98e64ed82f5acf0f2931babf69212ef52ddd37"
|
||||||
|
@ -10243,6 +10241,13 @@ koa-static@5.0.0, koa-static@^5.0.0:
|
||||||
debug "^3.1.0"
|
debug "^3.1.0"
|
||||||
koa-send "^5.0.0"
|
koa-send "^5.0.0"
|
||||||
|
|
||||||
|
koa-useragent@^4.1.0:
|
||||||
|
version "4.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/koa-useragent/-/koa-useragent-4.1.0.tgz#d3f128b552c6da3e5e9e9e9c887b2922b16e4468"
|
||||||
|
integrity sha512-x/HUDZ1zAmNNh5hA9hHbPm9p3UVg2prlpHzxCXQCzbibrNS0kmj7MkCResCbAbG7ZT6FVxNSMjR94ZGamdMwxA==
|
||||||
|
dependencies:
|
||||||
|
express-useragent "^1.0.15"
|
||||||
|
|
||||||
koa-views@^7.0.1:
|
koa-views@^7.0.1:
|
||||||
version "7.0.2"
|
version "7.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/koa-views/-/koa-views-7.0.2.tgz#c96fd9e2143ef00c29dc5160c5ed639891aa723d"
|
resolved "https://registry.yarnpkg.com/koa-views/-/koa-views-7.0.2.tgz#c96fd9e2143ef00c29dc5160c5ed639891aa723d"
|
||||||
|
@ -10706,7 +10711,12 @@ lru-cache@^6.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
yallist "^4.0.0"
|
yallist "^4.0.0"
|
||||||
|
|
||||||
lru-cache@^7.14.0, lru-cache@^7.14.1:
|
lru-cache@^7.14.0:
|
||||||
|
version "7.16.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.16.1.tgz#7acea16fecd9ed11430e78443c2bb81a06d3dea9"
|
||||||
|
integrity sha512-9kkuMZHnLH/8qXARvYSjNvq8S1GYFFzynQTAfKeaJ0sIrR3PUPuu37Z+EiIANiZBvpfTf2B5y8ecDLSMWlLv+w==
|
||||||
|
|
||||||
|
lru-cache@^7.14.1:
|
||||||
version "7.14.1"
|
version "7.14.1"
|
||||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.14.1.tgz#8da8d2f5f59827edb388e63e459ac23d6d408fea"
|
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.14.1.tgz#8da8d2f5f59827edb388e63e459ac23d6d408fea"
|
||||||
integrity sha512-ysxwsnTKdAx96aTRdhDOCQfDgbHnt8SK0KY8SEjO0wHinhWOFTESbjVCMPbU1uGXg/ch4lifqx0wfjOawU2+WA==
|
integrity sha512-ysxwsnTKdAx96aTRdhDOCQfDgbHnt8SK0KY8SEjO0wHinhWOFTESbjVCMPbU1uGXg/ch4lifqx0wfjOawU2+WA==
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
import { Event, AuditedEventFriendlyName } from "../../../sdk"
|
||||||
|
import {
|
||||||
|
PaginationResponse,
|
||||||
|
PaginationRequest,
|
||||||
|
BasicPaginationRequest,
|
||||||
|
} from "../"
|
||||||
|
import { User, App } from "../../../"
|
||||||
|
|
||||||
|
export interface AuditLogSearchParams {
|
||||||
|
userIds?: string[]
|
||||||
|
appIds?: string[]
|
||||||
|
events?: Event[]
|
||||||
|
startDate?: string
|
||||||
|
endDate?: string
|
||||||
|
fullSearch?: string
|
||||||
|
bookmark?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DownloadAuditLogsRequest extends AuditLogSearchParams {}
|
||||||
|
|
||||||
|
export interface SearchAuditLogsRequest
|
||||||
|
extends BasicPaginationRequest,
|
||||||
|
AuditLogSearchParams {}
|
||||||
|
|
||||||
|
export enum AuditLogResourceStatus {
|
||||||
|
DELETED = "deleted",
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuditLogEnriched {
|
||||||
|
app?: App | { _id: string; status: AuditLogResourceStatus }
|
||||||
|
user: User | { _id: string; status: AuditLogResourceStatus }
|
||||||
|
event: Event
|
||||||
|
timestamp: string
|
||||||
|
name: string
|
||||||
|
metadata: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SearchAuditLogsResponse extends PaginationResponse {
|
||||||
|
data: AuditLogEnriched[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DefinitionsAuditLogsResponse {
|
||||||
|
events: Record<string, string>
|
||||||
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
export * from "./environmentVariables"
|
export * from "./environmentVariables"
|
||||||
|
export * from "./auditLogs"
|
||||||
export * from "./events"
|
export * from "./events"
|
||||||
export * from "./configs"
|
export * from "./configs"
|
||||||
|
|
|
@ -5,3 +5,4 @@ export * from "./errors"
|
||||||
export * from "./schedule"
|
export * from "./schedule"
|
||||||
export * from "./app"
|
export * from "./app"
|
||||||
export * from "./global"
|
export * from "./global"
|
||||||
|
export * from "./pagination"
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
export enum SortOrder {
|
||||||
|
ASCENDING = "ascending",
|
||||||
|
DESCENDING = "descending",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum SortType {
|
||||||
|
STRING = "string",
|
||||||
|
number = "number",
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BasicPaginationRequest {
|
||||||
|
bookmark?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaginationRequest extends BasicPaginationRequest {
|
||||||
|
limit?: number
|
||||||
|
sort?: {
|
||||||
|
order: SortOrder
|
||||||
|
column: string
|
||||||
|
type: SortType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaginationResponse {
|
||||||
|
bookmark: string
|
||||||
|
hasNextPage: boolean
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { Document } from "../document"
|
||||||
|
import { Event } from "../../sdk"
|
||||||
|
|
||||||
|
export const AuditLogSystemUser = "SYSTEM"
|
||||||
|
|
||||||
|
export interface AuditLogDoc extends Document {
|
||||||
|
appId?: string
|
||||||
|
event: Event
|
||||||
|
userId: string
|
||||||
|
timestamp: string
|
||||||
|
metadata: any
|
||||||
|
name: string
|
||||||
|
}
|
|
@ -6,3 +6,4 @@ export * from "./quotas"
|
||||||
export * from "./schedule"
|
export * from "./schedule"
|
||||||
export * from "./templates"
|
export * from "./templates"
|
||||||
export * from "./environmentVariables"
|
export * from "./environmentVariables"
|
||||||
|
export * from "./auditLogs"
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { Event, HostInfo } from "./events"
|
||||||
|
import { AuditLogDoc } from "../documents"
|
||||||
|
|
||||||
|
export type AuditWriteOpts = {
|
||||||
|
appId?: string
|
||||||
|
timestamp?: string | number
|
||||||
|
userId?: string
|
||||||
|
hostInfo?: HostInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AuditLogFn = (
|
||||||
|
event: Event,
|
||||||
|
metadata: any,
|
||||||
|
opts: AuditWriteOpts
|
||||||
|
) => Promise<AuditLogDoc | undefined>
|
||||||
|
|
||||||
|
export type AuditLogQueueEvent = {
|
||||||
|
event: Event
|
||||||
|
properties: any
|
||||||
|
opts: AuditWriteOpts
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
import { User, Account } from "../documents"
|
import { User, Account } from "../documents"
|
||||||
import { IdentityType } from "./events"
|
import { IdentityType, HostInfo } from "./events"
|
||||||
|
|
||||||
export interface BaseContext {
|
export interface BaseContext {
|
||||||
_id: string
|
_id: string
|
||||||
|
@ -16,6 +16,7 @@ export interface UserContext extends BaseContext, User {
|
||||||
_id: string
|
_id: string
|
||||||
tenantId: string
|
tenantId: string
|
||||||
account?: Account
|
account?: Account
|
||||||
|
hostInfo: HostInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
export type IdentityContext = BaseContext | AccountUserContext | UserContext
|
export type IdentityContext = BaseContext | AccountUserContext | UserContext
|
||||||
|
|
|
@ -1,5 +1,11 @@
|
||||||
import Nano from "@budibase/nano"
|
import Nano from "@budibase/nano"
|
||||||
import { AllDocsResponse, AnyDocument, Document } from "../"
|
import { AllDocsResponse, AnyDocument, Document } from "../"
|
||||||
|
import { Writable } from "stream"
|
||||||
|
|
||||||
|
export enum SearchIndex {
|
||||||
|
ROWS = "rows",
|
||||||
|
AUDIT = "audit",
|
||||||
|
}
|
||||||
|
|
||||||
export type PouchOptions = {
|
export type PouchOptions = {
|
||||||
inMemory?: boolean
|
inMemory?: boolean
|
||||||
|
@ -63,6 +69,18 @@ export const isDocument = (doc: any): doc is Document => {
|
||||||
return typeof doc === "object" && doc._id && doc._rev
|
return typeof doc === "object" && doc._id && doc._rev
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DatabaseDumpOpts {
|
||||||
|
filter?: (doc: AnyDocument) => boolean
|
||||||
|
batch_size?: number
|
||||||
|
batch_limit?: number
|
||||||
|
style?: "main_only" | "all_docs"
|
||||||
|
timeout?: number
|
||||||
|
doc_ids?: string[]
|
||||||
|
query_params?: any
|
||||||
|
view?: string
|
||||||
|
selector?: any
|
||||||
|
}
|
||||||
|
|
||||||
export interface Database {
|
export interface Database {
|
||||||
name: string
|
name: string
|
||||||
|
|
||||||
|
@ -87,7 +105,7 @@ export interface Database {
|
||||||
compact(): Promise<Nano.OkResponse | void>
|
compact(): Promise<Nano.OkResponse | void>
|
||||||
// these are all PouchDB related functions that are rarely used - in future
|
// these are all PouchDB related functions that are rarely used - in future
|
||||||
// should be replaced by better typed/non-pouch implemented methods
|
// should be replaced by better typed/non-pouch implemented methods
|
||||||
dump(...args: any[]): Promise<any>
|
dump(stream: Writable, opts?: DatabaseDumpOpts): Promise<any>
|
||||||
load(...args: any[]): Promise<any>
|
load(...args: any[]): Promise<any>
|
||||||
createIndex(...args: any[]): Promise<any>
|
createIndex(...args: any[]): Promise<any>
|
||||||
deleteIndex(...args: any[]): Promise<any>
|
deleteIndex(...args: any[]): Promise<any>
|
||||||
|
|
|
@ -3,50 +3,83 @@ import { BaseEvent } from "./event"
|
||||||
export interface AppCreatedEvent extends BaseEvent {
|
export interface AppCreatedEvent extends BaseEvent {
|
||||||
appId: string
|
appId: string
|
||||||
version: string
|
version: string
|
||||||
|
audited: {
|
||||||
|
name: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppUpdatedEvent extends BaseEvent {
|
export interface AppUpdatedEvent extends BaseEvent {
|
||||||
appId: string
|
appId: string
|
||||||
version: string
|
version: string
|
||||||
|
audited: {
|
||||||
|
name: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppDeletedEvent extends BaseEvent {
|
export interface AppDeletedEvent extends BaseEvent {
|
||||||
appId: string
|
appId: string
|
||||||
|
audited: {
|
||||||
|
name: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppPublishedEvent extends BaseEvent {
|
export interface AppPublishedEvent extends BaseEvent {
|
||||||
appId: string
|
appId: string
|
||||||
|
audited: {
|
||||||
|
name: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppUnpublishedEvent extends BaseEvent {
|
export interface AppUnpublishedEvent extends BaseEvent {
|
||||||
appId: string
|
appId: string
|
||||||
|
audited: {
|
||||||
|
name: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppFileImportedEvent extends BaseEvent {
|
export interface AppFileImportedEvent extends BaseEvent {
|
||||||
appId: string
|
appId: string
|
||||||
|
audited: {
|
||||||
|
name: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppTemplateImportedEvent extends BaseEvent {
|
export interface AppTemplateImportedEvent extends BaseEvent {
|
||||||
appId: string
|
appId: string
|
||||||
templateKey: string
|
templateKey: string
|
||||||
|
audited: {
|
||||||
|
name: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppVersionUpdatedEvent extends BaseEvent {
|
export interface AppVersionUpdatedEvent extends BaseEvent {
|
||||||
appId: string
|
appId: string
|
||||||
currentVersion: string
|
currentVersion: string
|
||||||
updatedToVersion: string
|
updatedToVersion: string
|
||||||
|
audited: {
|
||||||
|
name: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppVersionRevertedEvent extends BaseEvent {
|
export interface AppVersionRevertedEvent extends BaseEvent {
|
||||||
appId: string
|
appId: string
|
||||||
currentVersion: string
|
currentVersion: string
|
||||||
revertedToVersion: string
|
revertedToVersion: string
|
||||||
|
audited: {
|
||||||
|
name: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppRevertedEvent extends BaseEvent {
|
export interface AppRevertedEvent extends BaseEvent {
|
||||||
appId: string
|
appId: string
|
||||||
|
audited: {
|
||||||
|
name: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppExportedEvent extends BaseEvent {
|
export interface AppExportedEvent extends BaseEvent {
|
||||||
appId: string
|
appId: string
|
||||||
|
audited: {
|
||||||
|
name: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { BaseEvent } from "./event"
|
||||||
|
import { AuditLogSearchParams } from "../../api"
|
||||||
|
|
||||||
|
export interface AuditLogFilteredEvent extends BaseEvent {
|
||||||
|
filters: AuditLogSearchParams
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuditLogDownloadedEvent extends BaseEvent {
|
||||||
|
filters: AuditLogSearchParams
|
||||||
|
}
|
|
@ -7,10 +7,16 @@ export type SSOType = ConfigType.OIDC | ConfigType.GOOGLE
|
||||||
export interface LoginEvent extends BaseEvent {
|
export interface LoginEvent extends BaseEvent {
|
||||||
userId: string
|
userId: string
|
||||||
source: LoginSource
|
source: LoginSource
|
||||||
|
audited: {
|
||||||
|
email: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LogoutEvent extends BaseEvent {
|
export interface LogoutEvent extends BaseEvent {
|
||||||
userId: string
|
userId: string
|
||||||
|
audited: {
|
||||||
|
email?: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SSOCreatedEvent extends BaseEvent {
|
export interface SSOCreatedEvent extends BaseEvent {
|
||||||
|
|
|
@ -5,6 +5,9 @@ export interface AutomationCreatedEvent extends BaseEvent {
|
||||||
automationId: string
|
automationId: string
|
||||||
triggerId: string
|
triggerId: string
|
||||||
triggerType: string
|
triggerType: string
|
||||||
|
audited: {
|
||||||
|
name: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AutomationTriggerUpdatedEvent extends BaseEvent {
|
export interface AutomationTriggerUpdatedEvent extends BaseEvent {
|
||||||
|
@ -19,6 +22,9 @@ export interface AutomationDeletedEvent extends BaseEvent {
|
||||||
automationId: string
|
automationId: string
|
||||||
triggerId: string
|
triggerId: string
|
||||||
triggerType: string
|
triggerType: string
|
||||||
|
audited: {
|
||||||
|
name: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AutomationTestedEvent extends BaseEvent {
|
export interface AutomationTestedEvent extends BaseEvent {
|
||||||
|
@ -35,6 +41,9 @@ export interface AutomationStepCreatedEvent extends BaseEvent {
|
||||||
triggerType: string
|
triggerType: string
|
||||||
stepId: string
|
stepId: string
|
||||||
stepType: string
|
stepType: string
|
||||||
|
audited: {
|
||||||
|
name: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AutomationStepDeletedEvent extends BaseEvent {
|
export interface AutomationStepDeletedEvent extends BaseEvent {
|
||||||
|
@ -44,6 +53,9 @@ export interface AutomationStepDeletedEvent extends BaseEvent {
|
||||||
triggerType: string
|
triggerType: string
|
||||||
stepId: string
|
stepId: string
|
||||||
stepType: string
|
stepType: string
|
||||||
|
audited: {
|
||||||
|
name: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AutomationsRunEvent extends BaseEvent {
|
export interface AutomationsRunEvent extends BaseEvent {
|
||||||
|
|
|
@ -5,6 +5,7 @@ export interface AppBackupRestoreEvent extends BaseEvent {
|
||||||
appId: string
|
appId: string
|
||||||
restoreId: string
|
restoreId: string
|
||||||
backupCreatedAt: string
|
backupCreatedAt: string
|
||||||
|
name: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppBackupTriggeredEvent extends BaseEvent {
|
export interface AppBackupTriggeredEvent extends BaseEvent {
|
||||||
|
@ -12,4 +13,5 @@ export interface AppBackupTriggeredEvent extends BaseEvent {
|
||||||
appId: string
|
appId: string
|
||||||
trigger: AppBackupTrigger
|
trigger: AppBackupTrigger
|
||||||
type: AppBackupType
|
type: AppBackupType
|
||||||
|
name: string
|
||||||
}
|
}
|
||||||
|
|
|
@ -180,6 +180,190 @@ export enum Event {
|
||||||
ENVIRONMENT_VARIABLE_CREATED = "environment_variable:created",
|
ENVIRONMENT_VARIABLE_CREATED = "environment_variable:created",
|
||||||
ENVIRONMENT_VARIABLE_DELETED = "environment_variable:deleted",
|
ENVIRONMENT_VARIABLE_DELETED = "environment_variable:deleted",
|
||||||
ENVIRONMENT_VARIABLE_UPGRADE_PANEL_OPENED = "environment_variable:upgrade_panel_opened",
|
ENVIRONMENT_VARIABLE_UPGRADE_PANEL_OPENED = "environment_variable:upgrade_panel_opened",
|
||||||
|
|
||||||
|
// AUDIT LOG
|
||||||
|
AUDIT_LOGS_FILTERED = "audit_log:filtered",
|
||||||
|
AUDIT_LOGS_DOWNLOADED = "audit_log:downloaded",
|
||||||
|
}
|
||||||
|
|
||||||
|
// all events that are not audited have been added to this record as undefined, this means
|
||||||
|
// that Typescript can protect us against new events being added and auditing of those
|
||||||
|
// events not being considered. This might be a little ugly, but provides a level of
|
||||||
|
// Typescript build protection for the audit log feature, any new event also needs to be
|
||||||
|
// added to this map, during which the developer will need to consider if it should be
|
||||||
|
// a user facing event or not.
|
||||||
|
export const AuditedEventFriendlyName: Record<Event, string | undefined> = {
|
||||||
|
// USER
|
||||||
|
[Event.USER_CREATED]: `User "{{ email }}" created`,
|
||||||
|
[Event.USER_UPDATED]: `User "{{ email }}" updated`,
|
||||||
|
[Event.USER_DELETED]: `User "{{ email }}" deleted`,
|
||||||
|
[Event.USER_PERMISSION_ADMIN_ASSIGNED]: `User "{{ email }}" admin role assigned`,
|
||||||
|
[Event.USER_PERMISSION_ADMIN_REMOVED]: `User "{{ email }}" admin role removed`,
|
||||||
|
[Event.USER_PERMISSION_BUILDER_ASSIGNED]: `User "{{ email }}" builder role assigned`,
|
||||||
|
[Event.USER_PERMISSION_BUILDER_REMOVED]: `User "{{ email }}" builder role removed`,
|
||||||
|
[Event.USER_INVITED]: `User "{{ email }}" invited`,
|
||||||
|
[Event.USER_INVITED_ACCEPTED]: `User "{{ email }}" accepted invite`,
|
||||||
|
[Event.USER_PASSWORD_UPDATED]: `User "{{ email }}" password updated`,
|
||||||
|
[Event.USER_PASSWORD_RESET_REQUESTED]: `User "{{ email }}" password reset requested`,
|
||||||
|
[Event.USER_PASSWORD_RESET]: `User "{{ email }}" password reset`,
|
||||||
|
[Event.USER_GROUP_CREATED]: `User group "{{ name }}" created`,
|
||||||
|
[Event.USER_GROUP_UPDATED]: `User group "{{ name }}" updated`,
|
||||||
|
[Event.USER_GROUP_DELETED]: `User group "{{ name }}" deleted`,
|
||||||
|
[Event.USER_GROUP_USERS_ADDED]: `User group "{{ name }}" {{ count }} users added`,
|
||||||
|
[Event.USER_GROUP_USERS_REMOVED]: `User group "{{ name }}" {{ count }} users removed`,
|
||||||
|
[Event.USER_GROUP_PERMISSIONS_EDITED]: `User group "{{ name }}" permissions edited`,
|
||||||
|
[Event.USER_PASSWORD_FORCE_RESET]: undefined,
|
||||||
|
[Event.USER_GROUP_ONBOARDING]: undefined,
|
||||||
|
[Event.USER_ONBOARDING_COMPLETE]: undefined,
|
||||||
|
|
||||||
|
// EMAIL
|
||||||
|
[Event.EMAIL_SMTP_CREATED]: `Email configuration created`,
|
||||||
|
[Event.EMAIL_SMTP_UPDATED]: `Email configuration updated`,
|
||||||
|
|
||||||
|
// AUTH
|
||||||
|
[Event.AUTH_SSO_CREATED]: `SSO configuration created`,
|
||||||
|
[Event.AUTH_SSO_UPDATED]: `SSO configuration updated`,
|
||||||
|
[Event.AUTH_SSO_ACTIVATED]: `SSO configuration activated`,
|
||||||
|
[Event.AUTH_SSO_DEACTIVATED]: `SSO configuration deactivated`,
|
||||||
|
[Event.AUTH_LOGIN]: `User "{{ email }}" logged in`,
|
||||||
|
[Event.AUTH_LOGOUT]: `User "{{ email }}" logged out`,
|
||||||
|
|
||||||
|
// ORG
|
||||||
|
[Event.ORG_NAME_UPDATED]: `Organisation name updated`,
|
||||||
|
[Event.ORG_LOGO_UPDATED]: `Organisation logo updated`,
|
||||||
|
[Event.ORG_PLATFORM_URL_UPDATED]: `Organisation platform URL updated`,
|
||||||
|
|
||||||
|
// APP
|
||||||
|
[Event.APP_CREATED]: `App "{{ name }}" created`,
|
||||||
|
[Event.APP_UPDATED]: `App "{{ name }}" updated`,
|
||||||
|
[Event.APP_DELETED]: `App "{{ name }}" deleted`,
|
||||||
|
[Event.APP_PUBLISHED]: `App "{{ name }}" published`,
|
||||||
|
[Event.APP_UNPUBLISHED]: `App "{{ name }}" unpublished`,
|
||||||
|
[Event.APP_TEMPLATE_IMPORTED]: `App "{{ name }}" template imported`,
|
||||||
|
[Event.APP_FILE_IMPORTED]: `App "{{ name }}" file imported`,
|
||||||
|
[Event.APP_VERSION_UPDATED]: `App "{{ name }}" version updated`,
|
||||||
|
[Event.APP_VERSION_REVERTED]: `App "{{ name }}" version reverted`,
|
||||||
|
[Event.APP_REVERTED]: `App "{{ name }}" reverted`,
|
||||||
|
[Event.APP_EXPORTED]: `App "{{ name }}" exported`,
|
||||||
|
[Event.APP_BACKUP_RESTORED]: `App backup "{{ name }}" restored`,
|
||||||
|
[Event.APP_BACKUP_TRIGGERED]: `App backup "{{ name }}" triggered`,
|
||||||
|
|
||||||
|
// DATASOURCE
|
||||||
|
[Event.DATASOURCE_CREATED]: `Datasource created`,
|
||||||
|
[Event.DATASOURCE_UPDATED]: `Datasource updated`,
|
||||||
|
[Event.DATASOURCE_DELETED]: `Datasource deleted`,
|
||||||
|
|
||||||
|
// QUERY
|
||||||
|
[Event.QUERY_CREATED]: `Query created`,
|
||||||
|
[Event.QUERY_UPDATED]: `Query updated`,
|
||||||
|
[Event.QUERY_DELETED]: `Query deleted`,
|
||||||
|
[Event.QUERY_IMPORT]: `Query import`,
|
||||||
|
[Event.QUERIES_RUN]: undefined,
|
||||||
|
[Event.QUERY_PREVIEWED]: undefined,
|
||||||
|
|
||||||
|
// TABLE
|
||||||
|
[Event.TABLE_CREATED]: `Table "{{ name }}" created`,
|
||||||
|
[Event.TABLE_UPDATED]: `Table "{{ name }}" updated`,
|
||||||
|
[Event.TABLE_DELETED]: `Table "{{ name }}" deleted`,
|
||||||
|
[Event.TABLE_EXPORTED]: `Table "{{ name }}" exported`,
|
||||||
|
[Event.TABLE_IMPORTED]: `Table "{{ name }}" imported`,
|
||||||
|
[Event.TABLE_DATA_IMPORTED]: `Data imported to table`,
|
||||||
|
|
||||||
|
// ROWS
|
||||||
|
[Event.ROWS_CREATED]: `Rows created`,
|
||||||
|
[Event.ROWS_IMPORTED]: `Rows imported`,
|
||||||
|
|
||||||
|
// AUTOMATION
|
||||||
|
[Event.AUTOMATION_CREATED]: `Automation "{{ name }}" created`,
|
||||||
|
[Event.AUTOMATION_DELETED]: `Automation "{{ name }}" deleted`,
|
||||||
|
[Event.AUTOMATION_STEP_CREATED]: `Automation "{{ name }}" step added`,
|
||||||
|
[Event.AUTOMATION_STEP_DELETED]: `Automation "{{ name }}" step removed`,
|
||||||
|
[Event.AUTOMATION_TESTED]: undefined,
|
||||||
|
[Event.AUTOMATIONS_RUN]: undefined,
|
||||||
|
[Event.AUTOMATION_TRIGGER_UPDATED]: undefined,
|
||||||
|
|
||||||
|
// SCREEN
|
||||||
|
[Event.SCREEN_CREATED]: `Screen "{{ name }}" created`,
|
||||||
|
[Event.SCREEN_DELETED]: `Screen "{{ name }}" deleted`,
|
||||||
|
|
||||||
|
// COMPONENT
|
||||||
|
[Event.COMPONENT_CREATED]: `Component created`,
|
||||||
|
[Event.COMPONENT_DELETED]: `Component deleted`,
|
||||||
|
|
||||||
|
// ENVIRONMENT VARIABLE
|
||||||
|
[Event.ENVIRONMENT_VARIABLE_CREATED]: `Environment variable created`,
|
||||||
|
[Event.ENVIRONMENT_VARIABLE_DELETED]: `Environment variable deleted`,
|
||||||
|
[Event.ENVIRONMENT_VARIABLE_UPGRADE_PANEL_OPENED]: undefined,
|
||||||
|
|
||||||
|
// PLUGIN
|
||||||
|
[Event.PLUGIN_IMPORTED]: `Plugin imported`,
|
||||||
|
[Event.PLUGIN_DELETED]: `Plugin deleted`,
|
||||||
|
[Event.PLUGIN_INIT]: undefined,
|
||||||
|
|
||||||
|
// ROLE - NOT AUDITED
|
||||||
|
[Event.ROLE_CREATED]: undefined,
|
||||||
|
[Event.ROLE_UPDATED]: undefined,
|
||||||
|
[Event.ROLE_DELETED]: undefined,
|
||||||
|
[Event.ROLE_ASSIGNED]: undefined,
|
||||||
|
[Event.ROLE_UNASSIGNED]: undefined,
|
||||||
|
|
||||||
|
// LICENSE - NOT AUDITED
|
||||||
|
[Event.LICENSE_PLAN_CHANGED]: undefined,
|
||||||
|
[Event.LICENSE_TIER_CHANGED]: undefined,
|
||||||
|
[Event.LICENSE_ACTIVATED]: undefined,
|
||||||
|
[Event.LICENSE_PAYMENT_FAILED]: undefined,
|
||||||
|
[Event.LICENSE_PAYMENT_RECOVERED]: undefined,
|
||||||
|
[Event.LICENSE_CHECKOUT_OPENED]: undefined,
|
||||||
|
[Event.LICENSE_CHECKOUT_SUCCESS]: undefined,
|
||||||
|
[Event.LICENSE_PORTAL_OPENED]: undefined,
|
||||||
|
|
||||||
|
// ACCOUNT - NOT AUDITED
|
||||||
|
[Event.ACCOUNT_CREATED]: undefined,
|
||||||
|
[Event.ACCOUNT_DELETED]: undefined,
|
||||||
|
[Event.ACCOUNT_VERIFIED]: undefined,
|
||||||
|
|
||||||
|
// BACKFILL - NOT AUDITED
|
||||||
|
[Event.APP_BACKFILL_SUCCEEDED]: undefined,
|
||||||
|
[Event.APP_BACKFILL_FAILED]: undefined,
|
||||||
|
[Event.TENANT_BACKFILL_SUCCEEDED]: undefined,
|
||||||
|
[Event.TENANT_BACKFILL_FAILED]: undefined,
|
||||||
|
[Event.INSTALLATION_BACKFILL_SUCCEEDED]: undefined,
|
||||||
|
[Event.INSTALLATION_BACKFILL_FAILED]: undefined,
|
||||||
|
|
||||||
|
// LAYOUT - NOT AUDITED
|
||||||
|
[Event.LAYOUT_CREATED]: undefined,
|
||||||
|
[Event.LAYOUT_DELETED]: undefined,
|
||||||
|
|
||||||
|
// VIEW - NOT AUDITED
|
||||||
|
[Event.VIEW_CREATED]: undefined,
|
||||||
|
[Event.VIEW_UPDATED]: undefined,
|
||||||
|
[Event.VIEW_DELETED]: undefined,
|
||||||
|
[Event.VIEW_EXPORTED]: undefined,
|
||||||
|
[Event.VIEW_FILTER_CREATED]: undefined,
|
||||||
|
[Event.VIEW_FILTER_UPDATED]: undefined,
|
||||||
|
[Event.VIEW_FILTER_DELETED]: undefined,
|
||||||
|
[Event.VIEW_CALCULATION_CREATED]: undefined,
|
||||||
|
[Event.VIEW_CALCULATION_UPDATED]: undefined,
|
||||||
|
[Event.VIEW_CALCULATION_DELETED]: undefined,
|
||||||
|
|
||||||
|
// SERVED - NOT AUDITED
|
||||||
|
[Event.SERVED_BUILDER]: undefined,
|
||||||
|
[Event.SERVED_APP]: undefined,
|
||||||
|
[Event.SERVED_APP_PREVIEW]: undefined,
|
||||||
|
|
||||||
|
// ANALYTICS - NOT AUDITED
|
||||||
|
[Event.ANALYTICS_OPT_OUT]: undefined,
|
||||||
|
[Event.ANALYTICS_OPT_IN]: undefined,
|
||||||
|
|
||||||
|
// INSTALLATION - NOT AUDITED
|
||||||
|
[Event.INSTALLATION_VERSION_CHECKED]: undefined,
|
||||||
|
[Event.INSTALLATION_VERSION_UPGRADED]: undefined,
|
||||||
|
[Event.INSTALLATION_VERSION_DOWNGRADED]: undefined,
|
||||||
|
[Event.INSTALLATION_FIRST_STARTUP]: undefined,
|
||||||
|
|
||||||
|
// AUDIT LOG - NOT AUDITED
|
||||||
|
[Event.AUDIT_LOGS_FILTERED]: undefined,
|
||||||
|
[Event.AUDIT_LOGS_DOWNLOADED]: undefined,
|
||||||
}
|
}
|
||||||
|
|
||||||
// properties added at the final stage of the event pipeline
|
// properties added at the final stage of the event pipeline
|
||||||
|
@ -191,6 +375,11 @@ export interface BaseEvent {
|
||||||
installationId?: string
|
installationId?: string
|
||||||
tenantId?: string
|
tenantId?: string
|
||||||
hosting?: Hosting
|
hosting?: Hosting
|
||||||
|
// any props in the audited section will be removed before passing events
|
||||||
|
// up out of system (purely for use with auditing)
|
||||||
|
audited?: {
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TableExportFormat = "json" | "csv"
|
export type TableExportFormat = "json" | "csv"
|
||||||
|
|
|
@ -34,6 +34,11 @@ export enum IdentityType {
|
||||||
INSTALLATION = "installation",
|
INSTALLATION = "installation",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface HostInfo {
|
||||||
|
ipAddress?: string
|
||||||
|
userAgent?: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface Identity {
|
export interface Identity {
|
||||||
id: string
|
id: string
|
||||||
type: IdentityType
|
type: IdentityType
|
||||||
|
@ -41,6 +46,7 @@ export interface Identity {
|
||||||
environment: string
|
environment: string
|
||||||
installationId?: string
|
installationId?: string
|
||||||
tenantId?: string
|
tenantId?: string
|
||||||
|
hostInfo?: HostInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserIdentity extends Identity {
|
export interface UserIdentity extends Identity {
|
||||||
|
|
|
@ -22,3 +22,4 @@ export * from "./userGroup"
|
||||||
export * from "./plugin"
|
export * from "./plugin"
|
||||||
export * from "./backup"
|
export * from "./backup"
|
||||||
export * from "./environmentVariable"
|
export * from "./environmentVariable"
|
||||||
|
export * from "./auditLog"
|
||||||
|
|
|
@ -4,10 +4,16 @@ export interface ScreenCreatedEvent extends BaseEvent {
|
||||||
screenId: string
|
screenId: string
|
||||||
layoutId?: string
|
layoutId?: string
|
||||||
roleId: string
|
roleId: string
|
||||||
|
audited: {
|
||||||
|
name: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ScreenDeletedEvent extends BaseEvent {
|
export interface ScreenDeletedEvent extends BaseEvent {
|
||||||
screenId: string
|
screenId: string
|
||||||
layoutId?: string
|
layoutId?: string
|
||||||
roleId: string
|
roleId: string
|
||||||
|
audited: {
|
||||||
|
name: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue