Merge branch 'master' into allow-public-view-attachment-uploads
This commit is contained in:
commit
d5bbd475a2
|
@ -222,9 +222,12 @@ export class DatabaseImpl implements Database {
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMultiple<T extends Document>(
|
async getMultiple<T extends Document>(
|
||||||
ids: string[],
|
ids?: string[],
|
||||||
opts?: { allowMissing?: boolean; excludeDocs?: boolean }
|
opts?: { allowMissing?: boolean; excludeDocs?: boolean }
|
||||||
): Promise<T[]> {
|
): Promise<T[]> {
|
||||||
|
if (!ids || ids.length === 0) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
// get unique
|
// get unique
|
||||||
ids = [...new Set(ids)]
|
ids = [...new Set(ids)]
|
||||||
const includeDocs = !opts?.excludeDocs
|
const includeDocs = !opts?.excludeDocs
|
||||||
|
@ -249,7 +252,7 @@ export class DatabaseImpl implements Database {
|
||||||
if (!opts?.allowMissing && someMissing) {
|
if (!opts?.allowMissing && someMissing) {
|
||||||
const missing = response.rows.filter(row => rowUnavailable(row))
|
const missing = response.rows.filter(row => rowUnavailable(row))
|
||||||
const missingIds = missing.map(row => row.key).join(", ")
|
const missingIds = missing.map(row => row.key).join(", ")
|
||||||
throw new Error(`Unable to get documents: ${missingIds}`)
|
throw new Error(`Unable to get bulk documents: ${missingIds}`)
|
||||||
}
|
}
|
||||||
return rows.map(row => (includeDocs ? row.doc! : row.value))
|
return rows.map(row => (includeDocs ? row.doc! : row.value))
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,13 +52,13 @@ export class DDInstrumentedDatabase implements Database {
|
||||||
}
|
}
|
||||||
|
|
||||||
getMultiple<T extends Document>(
|
getMultiple<T extends Document>(
|
||||||
ids: string[],
|
ids?: string[],
|
||||||
opts?: { allowMissing?: boolean | undefined } | undefined
|
opts?: { allowMissing?: boolean | undefined } | undefined
|
||||||
): Promise<T[]> {
|
): Promise<T[]> {
|
||||||
return tracer.trace("db.getMultiple", async span => {
|
return tracer.trace("db.getMultiple", async span => {
|
||||||
span.addTags({
|
span.addTags({
|
||||||
db_name: this.name,
|
db_name: this.name,
|
||||||
num_docs: ids.length,
|
num_docs: ids?.length || 0,
|
||||||
allow_missing: opts?.allowMissing,
|
allow_missing: opts?.allowMissing,
|
||||||
})
|
})
|
||||||
const docs = await this.db.getMultiple<T>(ids, opts)
|
const docs = await this.db.getMultiple<T>(ids, opts)
|
||||||
|
|
|
@ -26,8 +26,9 @@ import {
|
||||||
import {
|
import {
|
||||||
getAccountHolderFromUsers,
|
getAccountHolderFromUsers,
|
||||||
isAdmin,
|
isAdmin,
|
||||||
isCreator,
|
creatorsInList,
|
||||||
validateUniqueUser,
|
validateUniqueUser,
|
||||||
|
isCreatorAsync,
|
||||||
} from "./utils"
|
} from "./utils"
|
||||||
import {
|
import {
|
||||||
getFirstPlatformUser,
|
getFirstPlatformUser,
|
||||||
|
@ -261,8 +262,16 @@ export class UserDB {
|
||||||
}
|
}
|
||||||
|
|
||||||
const change = dbUser ? 0 : 1 // no change if there is existing user
|
const change = dbUser ? 0 : 1 // no change if there is existing user
|
||||||
const creatorsChange =
|
|
||||||
(await isCreator(dbUser)) !== (await isCreator(user)) ? 1 : 0
|
let creatorsChange = 0
|
||||||
|
if (dbUser) {
|
||||||
|
const [isDbUserCreator, isUserCreator] = await creatorsInList([
|
||||||
|
dbUser,
|
||||||
|
user,
|
||||||
|
])
|
||||||
|
creatorsChange = isDbUserCreator !== isUserCreator ? 1 : 0
|
||||||
|
}
|
||||||
|
|
||||||
return UserDB.quotas.addUsers(change, creatorsChange, async () => {
|
return UserDB.quotas.addUsers(change, creatorsChange, async () => {
|
||||||
if (!opts.isAccountHolder) {
|
if (!opts.isAccountHolder) {
|
||||||
await validateUniqueUser(email, tenantId)
|
await validateUniqueUser(email, tenantId)
|
||||||
|
@ -353,7 +362,7 @@ export class UserDB {
|
||||||
}
|
}
|
||||||
newUser.userGroups = groups || []
|
newUser.userGroups = groups || []
|
||||||
newUsers.push(newUser)
|
newUsers.push(newUser)
|
||||||
if (await isCreator(newUser)) {
|
if (await isCreatorAsync(newUser)) {
|
||||||
newCreators.push(newUser)
|
newCreators.push(newUser)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -453,10 +462,8 @@ export class UserDB {
|
||||||
}))
|
}))
|
||||||
const dbResponse = await usersCore.bulkUpdateGlobalUsers(toDelete)
|
const dbResponse = await usersCore.bulkUpdateGlobalUsers(toDelete)
|
||||||
|
|
||||||
const creatorsEval = await Promise.all(usersToDelete.map(isCreator))
|
const creatorsEval = await creatorsInList(usersToDelete)
|
||||||
const creatorsToDeleteCount = creatorsEval.filter(
|
const creatorsToDeleteCount = creatorsEval.filter(creator => creator).length
|
||||||
creator => !!creator
|
|
||||||
).length
|
|
||||||
|
|
||||||
const ssoUsersToDelete: AnyDocument[] = []
|
const ssoUsersToDelete: AnyDocument[] = []
|
||||||
for (let user of usersToDelete) {
|
for (let user of usersToDelete) {
|
||||||
|
@ -533,7 +540,7 @@ export class UserDB {
|
||||||
|
|
||||||
await db.remove(userId, dbUser._rev!)
|
await db.remove(userId, dbUser._rev!)
|
||||||
|
|
||||||
const creatorsToDelete = (await isCreator(dbUser)) ? 1 : 0
|
const creatorsToDelete = (await isCreatorAsync(dbUser)) ? 1 : 0
|
||||||
await UserDB.quotas.removeUsers(1, creatorsToDelete)
|
await UserDB.quotas.removeUsers(1, creatorsToDelete)
|
||||||
await eventHelpers.handleDeleteEvents(dbUser)
|
await eventHelpers.handleDeleteEvents(dbUser)
|
||||||
await cache.user.invalidateUser(userId)
|
await cache.user.invalidateUser(userId)
|
||||||
|
|
|
@ -2,39 +2,39 @@ import { User, UserGroup } from "@budibase/types"
|
||||||
import { generator, structures } from "../../../tests"
|
import { generator, structures } from "../../../tests"
|
||||||
import { DBTestConfiguration } from "../../../tests/extra"
|
import { DBTestConfiguration } from "../../../tests/extra"
|
||||||
import { getGlobalDB } from "../../context"
|
import { getGlobalDB } from "../../context"
|
||||||
import { isCreator } from "../utils"
|
import { isCreatorSync, creatorsInList } from "../utils"
|
||||||
|
|
||||||
const config = new DBTestConfiguration()
|
const config = new DBTestConfiguration()
|
||||||
|
|
||||||
describe("Users", () => {
|
describe("Users", () => {
|
||||||
it("User is a creator if it is configured as a global builder", async () => {
|
it("User is a creator if it is configured as a global builder", () => {
|
||||||
const user: User = structures.users.user({ builder: { global: true } })
|
const user: User = structures.users.user({ builder: { global: true } })
|
||||||
expect(await isCreator(user)).toBe(true)
|
expect(isCreatorSync(user, [])).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("User is a creator if it is configured as a global admin", async () => {
|
it("User is a creator if it is configured as a global admin", () => {
|
||||||
const user: User = structures.users.user({ admin: { global: true } })
|
const user: User = structures.users.user({ admin: { global: true } })
|
||||||
expect(await isCreator(user)).toBe(true)
|
expect(isCreatorSync(user, [])).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("User is a creator if it is configured with creator permission", async () => {
|
it("User is a creator if it is configured with creator permission", () => {
|
||||||
const user: User = structures.users.user({ builder: { creator: true } })
|
const user: User = structures.users.user({ builder: { creator: true } })
|
||||||
expect(await isCreator(user)).toBe(true)
|
expect(isCreatorSync(user, [])).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("User is a creator if it is a builder in some application", async () => {
|
it("User is a creator if it is a builder in some application", () => {
|
||||||
const user: User = structures.users.user({ builder: { apps: ["app1"] } })
|
const user: User = structures.users.user({ builder: { apps: ["app1"] } })
|
||||||
expect(await isCreator(user)).toBe(true)
|
expect(isCreatorSync(user, [])).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("User is a creator if it has CREATOR permission in some application", async () => {
|
it("User is a creator if it has CREATOR permission in some application", () => {
|
||||||
const user: User = structures.users.user({ roles: { app1: "CREATOR" } })
|
const user: User = structures.users.user({ roles: { app1: "CREATOR" } })
|
||||||
expect(await isCreator(user)).toBe(true)
|
expect(isCreatorSync(user, [])).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("User is a creator if it has ADMIN permission in some application", async () => {
|
it("User is a creator if it has ADMIN permission in some application", () => {
|
||||||
const user: User = structures.users.user({ roles: { app1: "ADMIN" } })
|
const user: User = structures.users.user({ roles: { app1: "ADMIN" } })
|
||||||
expect(await isCreator(user)).toBe(true)
|
expect(isCreatorSync(user, [])).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("User is a creator if it remains to a group with ADMIN permissions", async () => {
|
it("User is a creator if it remains to a group with ADMIN permissions", async () => {
|
||||||
|
@ -59,7 +59,7 @@ describe("Users", () => {
|
||||||
await db.put(group)
|
await db.put(group)
|
||||||
for (let user of users) {
|
for (let user of users) {
|
||||||
await db.put(user)
|
await db.put(user)
|
||||||
const creator = await isCreator(user)
|
const creator = (await creatorsInList([user]))[0]
|
||||||
expect(creator).toBe(true)
|
expect(creator).toBe(true)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -22,7 +22,7 @@ import {
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import * as context from "../context"
|
import * as context from "../context"
|
||||||
import { getGlobalDB } from "../context"
|
import { getGlobalDB } from "../context"
|
||||||
import { isCreator } from "./utils"
|
import { creatorsInList } from "./utils"
|
||||||
import { UserDB } from "./db"
|
import { UserDB } from "./db"
|
||||||
import { dataFilters } from "@budibase/shared-core"
|
import { dataFilters } from "@budibase/shared-core"
|
||||||
|
|
||||||
|
@ -305,8 +305,8 @@ export async function getCreatorCount() {
|
||||||
let creators = 0
|
let creators = 0
|
||||||
async function iterate(startPage?: string) {
|
async function iterate(startPage?: string) {
|
||||||
const page = await paginatedUsers({ bookmark: startPage })
|
const page = await paginatedUsers({ bookmark: startPage })
|
||||||
const creatorsEval = await Promise.all(page.data.map(isCreator))
|
const creatorsEval = await creatorsInList(page.data)
|
||||||
creators += creatorsEval.filter(creator => !!creator).length
|
creators += creatorsEval.filter(creator => creator).length
|
||||||
if (page.hasNextPage) {
|
if (page.hasNextPage) {
|
||||||
await iterate(page.nextPage)
|
await iterate(page.nextPage)
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,30 +16,47 @@ export const hasAdminPermissions = sdk.users.hasAdminPermissions
|
||||||
export const hasBuilderPermissions = sdk.users.hasBuilderPermissions
|
export const hasBuilderPermissions = sdk.users.hasBuilderPermissions
|
||||||
export const hasAppBuilderPermissions = sdk.users.hasAppBuilderPermissions
|
export const hasAppBuilderPermissions = sdk.users.hasAppBuilderPermissions
|
||||||
|
|
||||||
export async function isCreator(user?: User | ContextUser) {
|
export async function creatorsInList(
|
||||||
|
users: (User | ContextUser)[],
|
||||||
|
groups?: UserGroup[]
|
||||||
|
) {
|
||||||
|
const groupIds = [
|
||||||
|
...new Set(
|
||||||
|
users.filter(user => user.userGroups).flatMap(user => user.userGroups!)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
const db = context.getGlobalDB()
|
||||||
|
groups = await db.getMultiple<UserGroup>(groupIds, { allowMissing: true })
|
||||||
|
return users.map(user => isCreatorSync(user, groups))
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetches groups if no provided, but is async and shouldn't be looped with
|
||||||
|
export async function isCreatorAsync(user: User | ContextUser) {
|
||||||
|
let groups: UserGroup[] = []
|
||||||
|
if (user.userGroups) {
|
||||||
|
const db = context.getGlobalDB()
|
||||||
|
groups = await db.getMultiple<UserGroup>(user.userGroups)
|
||||||
|
}
|
||||||
|
return isCreatorSync(user, groups)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isCreatorSync(user: User | ContextUser, groups?: UserGroup[]) {
|
||||||
const isCreatorByUserDefinition = sdk.users.isCreator(user)
|
const isCreatorByUserDefinition = sdk.users.isCreator(user)
|
||||||
if (!isCreatorByUserDefinition && user) {
|
if (!isCreatorByUserDefinition && user) {
|
||||||
return await isCreatorByGroupMembership(user)
|
return isCreatorByGroupMembership(user, groups)
|
||||||
}
|
}
|
||||||
return isCreatorByUserDefinition
|
return isCreatorByUserDefinition
|
||||||
}
|
}
|
||||||
|
|
||||||
async function isCreatorByGroupMembership(user?: User | ContextUser) {
|
function isCreatorByGroupMembership(
|
||||||
const userGroups = user?.userGroups || []
|
user: User | ContextUser,
|
||||||
if (userGroups.length > 0) {
|
groups?: UserGroup[]
|
||||||
const db = context.getGlobalDB()
|
) {
|
||||||
const groups: UserGroup[] = []
|
const userGroups = groups?.filter(
|
||||||
for (let groupId of userGroups) {
|
group => user.userGroups?.indexOf(group._id!) !== -1
|
||||||
try {
|
)
|
||||||
const group = await db.get<UserGroup>(groupId)
|
if (userGroups && userGroups.length > 0) {
|
||||||
groups.push(group)
|
return userGroups.some(group =>
|
||||||
} catch (e: any) {
|
|
||||||
if (e.error !== "not_found") {
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return groups.some(group =>
|
|
||||||
Object.values(group.roles || {}).includes(BUILTIN_ROLE_IDS.ADMIN)
|
Object.values(group.roles || {}).includes(BUILTIN_ROLE_IDS.ADMIN)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,52 +0,0 @@
|
||||||
import { range } from "lodash/fp"
|
|
||||||
import { structures } from "../.."
|
|
||||||
|
|
||||||
jest.mock("../../../src/context")
|
|
||||||
jest.mock("../../../src/db")
|
|
||||||
|
|
||||||
import * as context from "../../../src/context"
|
|
||||||
import * as db from "../../../src/db"
|
|
||||||
|
|
||||||
import { getCreatorCount } from "../../../src/users/users"
|
|
||||||
|
|
||||||
describe("Users", () => {
|
|
||||||
let getGlobalDBMock: jest.SpyInstance
|
|
||||||
let paginationMock: jest.SpyInstance
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.resetAllMocks()
|
|
||||||
|
|
||||||
getGlobalDBMock = jest.spyOn(context, "getGlobalDB")
|
|
||||||
paginationMock = jest.spyOn(db, "pagination")
|
|
||||||
|
|
||||||
jest.spyOn(db, "getGlobalUserParams")
|
|
||||||
})
|
|
||||||
|
|
||||||
it("retrieves the number of creators", async () => {
|
|
||||||
const getUsers = (offset: number, limit: number, creators = false) => {
|
|
||||||
const opts = creators ? { builder: { global: true } } : undefined
|
|
||||||
return range(offset, limit).map(() => structures.users.user(opts))
|
|
||||||
}
|
|
||||||
const page1Data = getUsers(0, 8)
|
|
||||||
const page2Data = getUsers(8, 12, true)
|
|
||||||
getGlobalDBMock.mockImplementation(() => ({
|
|
||||||
name: "fake-db",
|
|
||||||
allDocs: () => ({
|
|
||||||
rows: [...page1Data, ...page2Data],
|
|
||||||
}),
|
|
||||||
}))
|
|
||||||
paginationMock.mockImplementationOnce(() => ({
|
|
||||||
data: page1Data,
|
|
||||||
hasNextPage: true,
|
|
||||||
nextPage: "1",
|
|
||||||
}))
|
|
||||||
paginationMock.mockImplementation(() => ({
|
|
||||||
data: page2Data,
|
|
||||||
hasNextPage: false,
|
|
||||||
nextPage: undefined,
|
|
||||||
}))
|
|
||||||
const creatorsCount = await getCreatorCount()
|
|
||||||
expect(creatorsCount).toBe(4)
|
|
||||||
expect(paginationMock).toHaveBeenCalledTimes(2)
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit b28dbd549284cf450be7f25ad85aadf614d08f0b
|
Subproject commit 2dd06c2fcb3cf10d5f16f5d8fe6cd344c8e905a5
|
|
@ -1,11 +1,13 @@
|
||||||
const setup = require("./utilities")
|
import { handleDataImport } from "../../controllers/table/utils"
|
||||||
const tableUtils = require("../../controllers/table/utils")
|
import TestConfiguration from "../../../tests/utilities/TestConfiguration"
|
||||||
|
import { AutoFieldSubType, FieldType, JsonFieldSubType } from "@budibase/types"
|
||||||
|
|
||||||
describe("run misc tests", () => {
|
describe("run misc tests", () => {
|
||||||
let request = setup.getRequest()
|
const config = new TestConfiguration()
|
||||||
let config = setup.getConfig()
|
|
||||||
|
|
||||||
afterAll(setup.afterAll)
|
afterAll(() => {
|
||||||
|
config.end()
|
||||||
|
})
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await config.init()
|
await config.init()
|
||||||
|
@ -13,69 +15,67 @@ describe("run misc tests", () => {
|
||||||
|
|
||||||
describe("/bbtel", () => {
|
describe("/bbtel", () => {
|
||||||
it("check if analytics enabled", async () => {
|
it("check if analytics enabled", async () => {
|
||||||
const res = await request
|
const { enabled } = await config.api.misc.bbtel()
|
||||||
.get(`/api/bbtel`)
|
expect(enabled).toEqual(true)
|
||||||
.set(config.defaultHeaders())
|
|
||||||
.expect("Content-Type", /json/)
|
|
||||||
.expect(200)
|
|
||||||
expect(typeof res.body.enabled).toEqual("boolean")
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("/health", () => {
|
describe("/health", () => {
|
||||||
it("should confirm healthy", async () => {
|
it("should confirm healthy", async () => {
|
||||||
await request.get("/health").expect(200)
|
await config.api.misc.health()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("/version", () => {
|
describe("/version", () => {
|
||||||
it("should confirm version", async () => {
|
it("should confirm version", async () => {
|
||||||
const res = await request.get("/version").expect(200)
|
const version = await config.api.misc.version()
|
||||||
const text = res.text
|
if (version.includes("alpha")) {
|
||||||
if (text.includes("alpha")) {
|
expect(version.split(".").length).toEqual(4)
|
||||||
expect(text.split(".").length).toEqual(4)
|
|
||||||
} else {
|
} else {
|
||||||
expect(text.split(".").length).toEqual(3)
|
expect(version.split(".").length).toEqual(3)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("test table utilities", () => {
|
describe("test table utilities", () => {
|
||||||
it("should be able to import data", async () => {
|
it("should be able to import data", async () => {
|
||||||
return config.doInContext(null, async () => {
|
return config.doInContext("", async () => {
|
||||||
const table = await config.createTable({
|
const table = await config.createTable({
|
||||||
name: "table",
|
name: "table",
|
||||||
type: "table",
|
type: "table",
|
||||||
key: "name",
|
|
||||||
schema: {
|
schema: {
|
||||||
a: {
|
a: {
|
||||||
type: "string",
|
type: FieldType.STRING,
|
||||||
|
name: "a",
|
||||||
constraints: {
|
constraints: {
|
||||||
type: "string",
|
type: "string",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
b: {
|
b: {
|
||||||
type: "string",
|
name: "b",
|
||||||
|
type: FieldType.STRING,
|
||||||
constraints: {
|
constraints: {
|
||||||
type: "string",
|
type: "string",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
c: {
|
c: {
|
||||||
type: "string",
|
name: "c",
|
||||||
|
type: FieldType.STRING,
|
||||||
constraints: {
|
constraints: {
|
||||||
type: "string",
|
type: "string",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
d: {
|
d: {
|
||||||
type: "string",
|
name: "d",
|
||||||
|
type: FieldType.STRING,
|
||||||
constraints: {
|
constraints: {
|
||||||
type: "string",
|
type: "string",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
e: {
|
e: {
|
||||||
name: "Auto ID",
|
name: "Auto ID",
|
||||||
type: "number",
|
type: FieldType.NUMBER,
|
||||||
subtype: "autoID",
|
subtype: AutoFieldSubType.AUTO_ID,
|
||||||
icon: "ri-magic-line",
|
icon: "ri-magic-line",
|
||||||
autocolumn: true,
|
autocolumn: true,
|
||||||
constraints: {
|
constraints: {
|
||||||
|
@ -88,9 +88,9 @@ describe("run misc tests", () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
f: {
|
f: {
|
||||||
type: "array",
|
type: FieldType.ARRAY,
|
||||||
constraints: {
|
constraints: {
|
||||||
type: "array",
|
type: JsonFieldSubType.ARRAY,
|
||||||
presence: {
|
presence: {
|
||||||
allowEmpty: true,
|
allowEmpty: true,
|
||||||
},
|
},
|
||||||
|
@ -100,7 +100,7 @@ describe("run misc tests", () => {
|
||||||
sortable: false,
|
sortable: false,
|
||||||
},
|
},
|
||||||
g: {
|
g: {
|
||||||
type: "options",
|
type: FieldType.OPTIONS,
|
||||||
constraints: {
|
constraints: {
|
||||||
type: "string",
|
type: "string",
|
||||||
presence: false,
|
presence: false,
|
||||||
|
@ -118,16 +118,18 @@ describe("run misc tests", () => {
|
||||||
{ a: "13", b: "14", c: "15", d: "16", g: "Omega" },
|
{ a: "13", b: "14", c: "15", d: "16", g: "Omega" },
|
||||||
]
|
]
|
||||||
// Shift specific row tests to the row spec
|
// Shift specific row tests to the row spec
|
||||||
await tableUtils.handleDataImport(table, {
|
await handleDataImport(table, { importRows, userId: "test" })
|
||||||
importRows,
|
|
||||||
user: { userId: "test" },
|
|
||||||
})
|
|
||||||
|
|
||||||
// 4 rows imported, the auto ID starts at 1
|
// 4 rows imported, the auto ID starts at 1
|
||||||
// We expect the handleDataImport function to update the lastID
|
// We expect the handleDataImport function to update the lastID
|
||||||
|
|
||||||
|
// @ts-expect-error - fields have type FieldSchema, not specific
|
||||||
|
// subtypes.
|
||||||
expect(table.schema.e.lastID).toEqual(4)
|
expect(table.schema.e.lastID).toEqual(4)
|
||||||
|
|
||||||
// Array/Multi - should have added a new value to the inclusion.
|
// Array/Multi - should have added a new value to the inclusion.
|
||||||
|
// @ts-expect-error - fields have type FieldSchema, not specific
|
||||||
|
// subtypes.
|
||||||
expect(table.schema.f.constraints.inclusion).toEqual([
|
expect(table.schema.f.constraints.inclusion).toEqual([
|
||||||
"Four",
|
"Four",
|
||||||
"One",
|
"One",
|
||||||
|
@ -136,6 +138,8 @@ describe("run misc tests", () => {
|
||||||
])
|
])
|
||||||
|
|
||||||
// Options - should have a new value in the inclusion
|
// Options - should have a new value in the inclusion
|
||||||
|
// @ts-expect-error - fields have type FieldSchema, not specific
|
||||||
|
// subtypes.
|
||||||
expect(table.schema.g.constraints.inclusion).toEqual([
|
expect(table.schema.g.constraints.inclusion).toEqual([
|
||||||
"Alpha",
|
"Alpha",
|
||||||
"Beta",
|
"Beta",
|
||||||
|
@ -143,25 +147,25 @@ describe("run misc tests", () => {
|
||||||
"Omega",
|
"Omega",
|
||||||
])
|
])
|
||||||
|
|
||||||
const rows = await config.getRows()
|
const rows = await config.api.row.fetch(table._id!)
|
||||||
expect(rows.length).toEqual(4)
|
expect(rows.length).toEqual(4)
|
||||||
|
|
||||||
const rowOne = rows.find(row => row.e === 1)
|
const rowOne = rows.find(row => row.e === 1)!
|
||||||
expect(rowOne.a).toEqual("1")
|
expect(rowOne.a).toEqual("1")
|
||||||
expect(rowOne.f).toEqual(["One"])
|
expect(rowOne.f).toEqual(["One"])
|
||||||
expect(rowOne.g).toEqual("Alpha")
|
expect(rowOne.g).toEqual("Alpha")
|
||||||
|
|
||||||
const rowTwo = rows.find(row => row.e === 2)
|
const rowTwo = rows.find(row => row.e === 2)!
|
||||||
expect(rowTwo.a).toEqual("5")
|
expect(rowTwo.a).toEqual("5")
|
||||||
expect(rowTwo.f).toEqual([])
|
expect(rowTwo.f).toEqual([])
|
||||||
expect(rowTwo.g).toEqual(undefined)
|
expect(rowTwo.g).toEqual(undefined)
|
||||||
|
|
||||||
const rowThree = rows.find(row => row.e === 3)
|
const rowThree = rows.find(row => row.e === 3)!
|
||||||
expect(rowThree.a).toEqual("9")
|
expect(rowThree.a).toEqual("9")
|
||||||
expect(rowThree.f).toEqual(["Two", "Four"])
|
expect(rowThree.f).toEqual(["Two", "Four"])
|
||||||
expect(rowThree.g).toEqual(undefined)
|
expect(rowThree.g).toEqual(undefined)
|
||||||
|
|
||||||
const rowFour = rows.find(row => row.e === 4)
|
const rowFour = rows.find(row => row.e === 4)!
|
||||||
expect(rowFour.a).toEqual("13")
|
expect(rowFour.a).toEqual("13")
|
||||||
expect(rowFour.f).toEqual(undefined)
|
expect(rowFour.f).toEqual(undefined)
|
||||||
expect(rowFour.g).toEqual("Omega")
|
expect(rowFour.g).toEqual("Omega")
|
|
@ -34,7 +34,7 @@ const checkAuthorized = async (
|
||||||
const isCreatorApi = permType === PermissionType.CREATOR
|
const isCreatorApi = permType === PermissionType.CREATOR
|
||||||
const isBuilderApi = permType === PermissionType.BUILDER
|
const isBuilderApi = permType === PermissionType.BUILDER
|
||||||
const isGlobalBuilder = users.isGlobalBuilder(ctx.user)
|
const isGlobalBuilder = users.isGlobalBuilder(ctx.user)
|
||||||
const isCreator = await users.isCreator(ctx.user)
|
const isCreator = await users.isCreatorAsync(ctx.user)
|
||||||
const isBuilder = appId
|
const isBuilder = appId
|
||||||
? users.isBuilder(ctx.user, appId)
|
? users.isBuilder(ctx.user, appId)
|
||||||
: users.hasBuilderPermissions(ctx.user)
|
: users.hasBuilderPermissions(ctx.user)
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
import env from "../environment"
|
|
||||||
import { Ctx } from "@budibase/types"
|
|
||||||
|
|
||||||
// if added as a middleware will stop requests unless builder is in self host mode
|
|
||||||
// or cloud is in self host
|
|
||||||
export default async (ctx: Ctx, next: any) => {
|
|
||||||
if (env.SELF_HOSTED) {
|
|
||||||
await next()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ctx.throw(400, "Endpoint unavailable in cloud hosting.")
|
|
||||||
}
|
|
|
@ -1,75 +0,0 @@
|
||||||
import ensureTenantAppOwnership from "../ensureTenantAppOwnership"
|
|
||||||
import { tenancy, utils } from "@budibase/backend-core"
|
|
||||||
|
|
||||||
jest.mock("@budibase/backend-core", () => ({
|
|
||||||
...jest.requireActual("@budibase/backend-core"),
|
|
||||||
tenancy: {
|
|
||||||
getTenantId: jest.fn(),
|
|
||||||
},
|
|
||||||
utils: {
|
|
||||||
getAppIdFromCtx: jest.fn(),
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
|
|
||||||
class TestConfiguration {
|
|
||||||
constructor(appId = "tenant_1") {
|
|
||||||
this.next = jest.fn()
|
|
||||||
this.throw = jest.fn()
|
|
||||||
this.middleware = ensureTenantAppOwnership
|
|
||||||
|
|
||||||
this.ctx = {
|
|
||||||
next: this.next,
|
|
||||||
throw: this.throw,
|
|
||||||
}
|
|
||||||
|
|
||||||
utils.getAppIdFromCtx.mockResolvedValue(appId)
|
|
||||||
}
|
|
||||||
|
|
||||||
async executeMiddleware() {
|
|
||||||
return this.middleware(this.ctx, this.next)
|
|
||||||
}
|
|
||||||
|
|
||||||
afterEach() {
|
|
||||||
jest.clearAllMocks()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("Ensure Tenant Ownership Middleware", () => {
|
|
||||||
let config
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
config = new TestConfiguration()
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
config.afterEach()
|
|
||||||
})
|
|
||||||
|
|
||||||
it("calls next() when appId matches tenant ID", async () => {
|
|
||||||
tenancy.getTenantId.mockReturnValue("tenant_1")
|
|
||||||
|
|
||||||
await config.executeMiddleware()
|
|
||||||
|
|
||||||
expect(utils.getAppIdFromCtx).toHaveBeenCalledWith(config.ctx)
|
|
||||||
expect(config.next).toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
it("throws when tenant appId does not match tenant ID", async () => {
|
|
||||||
const appId = "app_dev_tenant3_fce449c4d75b4e4a9c7a6980d82a3e22"
|
|
||||||
utils.getAppIdFromCtx.mockResolvedValue(appId)
|
|
||||||
tenancy.getTenantId.mockReturnValue("tenant_2")
|
|
||||||
|
|
||||||
await config.executeMiddleware()
|
|
||||||
|
|
||||||
expect(utils.getAppIdFromCtx).toHaveBeenCalledWith(config.ctx)
|
|
||||||
expect(config.throw).toHaveBeenCalledWith(403, "Unauthorized")
|
|
||||||
})
|
|
||||||
|
|
||||||
it("throws 400 when appId is missing", async () => {
|
|
||||||
utils.getAppIdFromCtx.mockResolvedValue(null)
|
|
||||||
|
|
||||||
await config.executeMiddleware()
|
|
||||||
|
|
||||||
expect(config.throw).toHaveBeenCalledWith(400, "appId must be provided")
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
import { UserCtx } from "@budibase/types"
|
||||||
|
import ensureTenantAppOwnership from "../ensureTenantAppOwnership"
|
||||||
|
import { context, Header, HTTPError } from "@budibase/backend-core"
|
||||||
|
|
||||||
|
function ctx(opts?: { appId: string }) {
|
||||||
|
const ctx = {
|
||||||
|
throw: (status: number, message: string) => {
|
||||||
|
throw new HTTPError(message, status)
|
||||||
|
},
|
||||||
|
path: "",
|
||||||
|
request: {
|
||||||
|
headers: {},
|
||||||
|
},
|
||||||
|
} as unknown as UserCtx
|
||||||
|
if (opts?.appId) {
|
||||||
|
ctx.request.headers[Header.APP_ID] = opts.appId
|
||||||
|
}
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("Ensure Tenant Ownership Middleware", () => {
|
||||||
|
const tenantId = "tenant1"
|
||||||
|
const appId = `app_dev_${tenantId}_fce449c4d75b4e4a9c7a6980d82a3e22`
|
||||||
|
|
||||||
|
it("calls next() when appId matches tenant ID", async () => {
|
||||||
|
await context.doInTenant(tenantId, async () => {
|
||||||
|
let called = false
|
||||||
|
await ensureTenantAppOwnership(ctx({ appId }), () => {
|
||||||
|
called = true
|
||||||
|
})
|
||||||
|
expect(called).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("throws when tenant appId does not match tenant ID", async () => {
|
||||||
|
let called = false
|
||||||
|
await expect(async () => {
|
||||||
|
await context.doInTenant("tenant_2", async () => {
|
||||||
|
await ensureTenantAppOwnership(ctx({ appId }), () => {
|
||||||
|
called = true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}).rejects.toThrow("Unauthorized")
|
||||||
|
expect(called).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("throws 400 when appId is missing", async () => {
|
||||||
|
await expect(ensureTenantAppOwnership(ctx(), () => {})).rejects.toThrow(
|
||||||
|
"appId must be provided"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
|
@ -1,105 +0,0 @@
|
||||||
const {
|
|
||||||
paramResource,
|
|
||||||
paramSubResource,
|
|
||||||
bodyResource,
|
|
||||||
bodySubResource,
|
|
||||||
ResourceIdGetter,
|
|
||||||
} = require("../resourceId")
|
|
||||||
|
|
||||||
class TestConfiguration {
|
|
||||||
constructor(middleware) {
|
|
||||||
this.middleware = middleware
|
|
||||||
this.ctx = {
|
|
||||||
request: {},
|
|
||||||
}
|
|
||||||
this.next = jest.fn()
|
|
||||||
}
|
|
||||||
|
|
||||||
setParams(params) {
|
|
||||||
this.ctx.params = params
|
|
||||||
}
|
|
||||||
|
|
||||||
setBody(body) {
|
|
||||||
this.ctx.body = body
|
|
||||||
}
|
|
||||||
|
|
||||||
executeMiddleware() {
|
|
||||||
return this.middleware(this.ctx, this.next)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("resourceId middleware", () => {
|
|
||||||
it("calls next() when there is no request object to parse", () => {
|
|
||||||
const config = new TestConfiguration(paramResource("main"))
|
|
||||||
|
|
||||||
config.executeMiddleware()
|
|
||||||
|
|
||||||
expect(config.next).toHaveBeenCalled()
|
|
||||||
expect(config.ctx.resourceId).toBeUndefined()
|
|
||||||
})
|
|
||||||
|
|
||||||
it("generates a resourceId middleware for context query parameters", () => {
|
|
||||||
const config = new TestConfiguration(paramResource("main"))
|
|
||||||
config.setParams({
|
|
||||||
main: "test",
|
|
||||||
})
|
|
||||||
|
|
||||||
config.executeMiddleware()
|
|
||||||
|
|
||||||
expect(config.ctx.resourceId).toEqual("test")
|
|
||||||
})
|
|
||||||
|
|
||||||
it("generates a resourceId middleware for context query sub parameters", () => {
|
|
||||||
const config = new TestConfiguration(paramSubResource("main", "sub"))
|
|
||||||
config.setParams({
|
|
||||||
main: "main",
|
|
||||||
sub: "test",
|
|
||||||
})
|
|
||||||
|
|
||||||
config.executeMiddleware()
|
|
||||||
|
|
||||||
expect(config.ctx.resourceId).toEqual("main")
|
|
||||||
expect(config.ctx.subResourceId).toEqual("test")
|
|
||||||
})
|
|
||||||
|
|
||||||
it("generates a resourceId middleware for context request body", () => {
|
|
||||||
const config = new TestConfiguration(bodyResource("main"))
|
|
||||||
config.setBody({
|
|
||||||
main: "test",
|
|
||||||
})
|
|
||||||
|
|
||||||
config.executeMiddleware()
|
|
||||||
|
|
||||||
expect(config.ctx.resourceId).toEqual("test")
|
|
||||||
})
|
|
||||||
|
|
||||||
it("generates a resourceId middleware for context request body sub fields", () => {
|
|
||||||
const config = new TestConfiguration(bodySubResource("main", "sub"))
|
|
||||||
config.setBody({
|
|
||||||
main: "main",
|
|
||||||
sub: "test",
|
|
||||||
})
|
|
||||||
|
|
||||||
config.executeMiddleware()
|
|
||||||
|
|
||||||
expect(config.ctx.resourceId).toEqual("main")
|
|
||||||
expect(config.ctx.subResourceId).toEqual("test")
|
|
||||||
})
|
|
||||||
|
|
||||||
it("parses resourceIds correctly for custom middlewares", () => {
|
|
||||||
const middleware = new ResourceIdGetter("body")
|
|
||||||
.mainResource("custom")
|
|
||||||
.subResource("customSub")
|
|
||||||
.build()
|
|
||||||
let config = new TestConfiguration(middleware)
|
|
||||||
config.setBody({
|
|
||||||
custom: "test",
|
|
||||||
customSub: "subtest",
|
|
||||||
})
|
|
||||||
|
|
||||||
config.executeMiddleware()
|
|
||||||
|
|
||||||
expect(config.ctx.resourceId).toEqual("test")
|
|
||||||
expect(config.ctx.subResourceId).toEqual("subtest")
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -0,0 +1,93 @@
|
||||||
|
import { Ctx } from "@budibase/types"
|
||||||
|
import {
|
||||||
|
paramResource,
|
||||||
|
paramSubResource,
|
||||||
|
bodyResource,
|
||||||
|
bodySubResource,
|
||||||
|
ResourceIdGetter,
|
||||||
|
} from "../resourceId"
|
||||||
|
|
||||||
|
describe("resourceId middleware", () => {
|
||||||
|
it("calls next() when there is no request object to parse", () => {
|
||||||
|
const ctx = { request: {} } as Ctx
|
||||||
|
let called = false
|
||||||
|
paramResource("main")(ctx, () => {
|
||||||
|
called = true
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(called).toBe(true)
|
||||||
|
expect(ctx.resourceId).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("generates a resourceId middleware for context query parameters", () => {
|
||||||
|
const ctx = { request: {}, params: { main: "test" } } as unknown as Ctx
|
||||||
|
let called = false
|
||||||
|
paramResource("main")(ctx, () => {
|
||||||
|
called = true
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(called).toBe(true)
|
||||||
|
expect(ctx.resourceId).toEqual("test")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("generates a resourceId middleware for context query sub parameters", () => {
|
||||||
|
const ctx = {
|
||||||
|
request: {},
|
||||||
|
params: { main: "main", sub: "test" },
|
||||||
|
} as unknown as Ctx
|
||||||
|
let called = false
|
||||||
|
paramSubResource("main", "sub")(ctx, () => {
|
||||||
|
called = true
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(called).toBe(true)
|
||||||
|
expect(ctx.resourceId).toEqual("main")
|
||||||
|
expect(ctx.subResourceId).toEqual("test")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("generates a resourceId middleware for context request body", () => {
|
||||||
|
const ctx = { request: {}, body: { main: "main" } } as unknown as Ctx
|
||||||
|
let called = false
|
||||||
|
bodyResource("main")(ctx, () => {
|
||||||
|
called = true
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(called).toBe(true)
|
||||||
|
expect(ctx.resourceId).toEqual("main")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("generates a resourceId middleware for context request body sub fields", () => {
|
||||||
|
const ctx = {
|
||||||
|
request: {},
|
||||||
|
body: { main: "main", sub: "test" },
|
||||||
|
} as unknown as Ctx
|
||||||
|
let called = false
|
||||||
|
bodySubResource("main", "sub")(ctx, () => {
|
||||||
|
called = true
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(called).toBe(true)
|
||||||
|
expect(ctx.resourceId).toEqual("main")
|
||||||
|
expect(ctx.subResourceId).toEqual("test")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("parses resourceIds correctly for custom middlewares", () => {
|
||||||
|
const middleware = new ResourceIdGetter("body")
|
||||||
|
.mainResource("custom")
|
||||||
|
.subResource("customSub")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
const ctx = {
|
||||||
|
request: {},
|
||||||
|
body: { custom: "test", customSub: "subTest" },
|
||||||
|
} as unknown as Ctx
|
||||||
|
let called = false
|
||||||
|
middleware(ctx, () => {
|
||||||
|
called = true
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(called).toBe(true)
|
||||||
|
expect(ctx.resourceId).toEqual("test")
|
||||||
|
expect(ctx.subResourceId).toEqual("subTest")
|
||||||
|
})
|
||||||
|
})
|
|
@ -1,43 +0,0 @@
|
||||||
const selfHostMiddleware = require("../selfhost").default
|
|
||||||
const env = require("../../environment")
|
|
||||||
jest.mock("../../environment")
|
|
||||||
|
|
||||||
class TestConfiguration {
|
|
||||||
constructor() {
|
|
||||||
this.next = jest.fn()
|
|
||||||
this.throw = jest.fn()
|
|
||||||
this.middleware = selfHostMiddleware
|
|
||||||
|
|
||||||
this.ctx = {
|
|
||||||
next: this.next,
|
|
||||||
throw: this.throw,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
executeMiddleware() {
|
|
||||||
return this.middleware(this.ctx, this.next)
|
|
||||||
}
|
|
||||||
|
|
||||||
afterEach() {
|
|
||||||
jest.clearAllMocks()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("Self host middleware", () => {
|
|
||||||
let config
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
config = new TestConfiguration()
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
config.afterEach()
|
|
||||||
})
|
|
||||||
|
|
||||||
it("calls next() when SELF_HOSTED env var is set", async () => {
|
|
||||||
env.SELF_HOSTED = 1
|
|
||||||
|
|
||||||
await config.executeMiddleware()
|
|
||||||
expect(config.next).toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -19,6 +19,7 @@ import { PluginAPI } from "./plugin"
|
||||||
import { WebhookAPI } from "./webhook"
|
import { WebhookAPI } from "./webhook"
|
||||||
import { EnvironmentAPI } from "./environment"
|
import { EnvironmentAPI } from "./environment"
|
||||||
import { UserPublicAPI } from "./public/user"
|
import { UserPublicAPI } from "./public/user"
|
||||||
|
import { MiscAPI } from "./misc"
|
||||||
|
|
||||||
export default class API {
|
export default class API {
|
||||||
application: ApplicationAPI
|
application: ApplicationAPI
|
||||||
|
@ -28,6 +29,7 @@ export default class API {
|
||||||
datasource: DatasourceAPI
|
datasource: DatasourceAPI
|
||||||
environment: EnvironmentAPI
|
environment: EnvironmentAPI
|
||||||
legacyView: LegacyViewAPI
|
legacyView: LegacyViewAPI
|
||||||
|
misc: MiscAPI
|
||||||
permission: PermissionAPI
|
permission: PermissionAPI
|
||||||
plugin: PluginAPI
|
plugin: PluginAPI
|
||||||
query: QueryAPI
|
query: QueryAPI
|
||||||
|
@ -53,6 +55,7 @@ export default class API {
|
||||||
this.datasource = new DatasourceAPI(config)
|
this.datasource = new DatasourceAPI(config)
|
||||||
this.environment = new EnvironmentAPI(config)
|
this.environment = new EnvironmentAPI(config)
|
||||||
this.legacyView = new LegacyViewAPI(config)
|
this.legacyView = new LegacyViewAPI(config)
|
||||||
|
this.misc = new MiscAPI(config)
|
||||||
this.permission = new PermissionAPI(config)
|
this.permission = new PermissionAPI(config)
|
||||||
this.plugin = new PluginAPI(config)
|
this.plugin = new PluginAPI(config)
|
||||||
this.query = new QueryAPI(config)
|
this.query = new QueryAPI(config)
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { AnalyticsEnabledResponse } from "@budibase/types"
|
||||||
|
import { Expectations, TestAPI } from "./base"
|
||||||
|
|
||||||
|
export class MiscAPI extends TestAPI {
|
||||||
|
health = async (expectations?: Expectations) => {
|
||||||
|
return await this._get<void>("/health", { expectations })
|
||||||
|
}
|
||||||
|
|
||||||
|
version = async (expectations?: Expectations) => {
|
||||||
|
return (await this._requestRaw("get", "/version", { expectations })).text
|
||||||
|
}
|
||||||
|
|
||||||
|
bbtel = async (expectations?: Expectations) => {
|
||||||
|
return await this._get<AnalyticsEnabledResponse>("/api/bbtel", {
|
||||||
|
expectations,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -136,7 +136,7 @@ export interface Database {
|
||||||
get<T extends Document>(id?: string): Promise<T>
|
get<T extends Document>(id?: string): Promise<T>
|
||||||
tryGet<T extends Document>(id?: string): Promise<T | undefined>
|
tryGet<T extends Document>(id?: string): Promise<T | undefined>
|
||||||
getMultiple<T extends Document>(
|
getMultiple<T extends Document>(
|
||||||
ids: string[],
|
ids?: string[],
|
||||||
opts?: { allowMissing?: boolean; excludeDocs?: boolean }
|
opts?: { allowMissing?: boolean; excludeDocs?: boolean }
|
||||||
): Promise<T[]>
|
): Promise<T[]>
|
||||||
remove(idOrDoc: Document): Promise<Nano.DocumentDestroyResponse>
|
remove(idOrDoc: Document): Promise<Nano.DocumentDestroyResponse>
|
||||||
|
|
Loading…
Reference in New Issue