Merge pull request #9493 from Budibase/fix/8236
CouchDB integration fixes
This commit is contained in:
commit
dfeb41ee53
5 changed files with 139 additions and 1039 deletions
|
@ -15,18 +15,47 @@ import { getCouchInfo } from "./connections"
|
|||
import { directCouchCall } from "./utils"
|
||||
import { getPouchDB } from "./pouchDB"
|
||||
import { WriteStream, ReadStream } from "fs"
|
||||
import { newid } from "../../newid"
|
||||
|
||||
function buildNano(couchInfo: { url: string; cookie: string }) {
|
||||
return Nano({
|
||||
url: couchInfo.url,
|
||||
requestDefaults: {
|
||||
headers: {
|
||||
Authorization: couchInfo.cookie,
|
||||
},
|
||||
},
|
||||
parseUrl: false,
|
||||
})
|
||||
}
|
||||
|
||||
export function DatabaseWithConnection(
|
||||
dbName: string,
|
||||
connection: string,
|
||||
opts?: DatabaseOpts
|
||||
) {
|
||||
if (!connection) {
|
||||
throw new Error("Must provide connection details")
|
||||
}
|
||||
return new DatabaseImpl(dbName, opts, connection)
|
||||
}
|
||||
|
||||
export class DatabaseImpl implements Database {
|
||||
public readonly name: string
|
||||
private static nano: Nano.ServerScope
|
||||
private readonly instanceNano?: Nano.ServerScope
|
||||
private readonly pouchOpts: DatabaseOpts
|
||||
|
||||
constructor(dbName?: string, opts?: DatabaseOpts) {
|
||||
constructor(dbName?: string, opts?: DatabaseOpts, connection?: string) {
|
||||
if (dbName == null) {
|
||||
throw new Error("Database name cannot be undefined.")
|
||||
}
|
||||
this.name = dbName
|
||||
this.pouchOpts = opts || {}
|
||||
if (connection) {
|
||||
const couchInfo = getCouchInfo(connection)
|
||||
this.instanceNano = buildNano(couchInfo)
|
||||
}
|
||||
if (!DatabaseImpl.nano) {
|
||||
DatabaseImpl.init()
|
||||
}
|
||||
|
@ -34,15 +63,7 @@ export class DatabaseImpl implements Database {
|
|||
|
||||
static init() {
|
||||
const couchInfo = getCouchInfo()
|
||||
DatabaseImpl.nano = Nano({
|
||||
url: couchInfo.url,
|
||||
requestDefaults: {
|
||||
headers: {
|
||||
Authorization: couchInfo.cookie,
|
||||
},
|
||||
},
|
||||
parseUrl: false,
|
||||
})
|
||||
DatabaseImpl.nano = buildNano(couchInfo)
|
||||
}
|
||||
|
||||
async exists() {
|
||||
|
@ -50,6 +71,10 @@ export class DatabaseImpl implements Database {
|
|||
return response.status === 200
|
||||
}
|
||||
|
||||
private nano() {
|
||||
return this.instanceNano || DatabaseImpl.nano
|
||||
}
|
||||
|
||||
async checkSetup() {
|
||||
let shouldCreate = !this.pouchOpts?.skip_setup
|
||||
// check exists in a lightweight fashion
|
||||
|
@ -58,9 +83,9 @@ export class DatabaseImpl implements Database {
|
|||
throw new Error("DB does not exist")
|
||||
}
|
||||
if (!exists) {
|
||||
await DatabaseImpl.nano.db.create(this.name)
|
||||
await this.nano().db.create(this.name)
|
||||
}
|
||||
return DatabaseImpl.nano.db.use(this.name)
|
||||
return this.nano().db.use(this.name)
|
||||
}
|
||||
|
||||
private async updateOutput(fnc: any) {
|
||||
|
@ -101,6 +126,13 @@ export class DatabaseImpl implements Database {
|
|||
return this.updateOutput(() => db.destroy(_id, _rev))
|
||||
}
|
||||
|
||||
async post(document: AnyDocument, opts?: DatabasePutOpts) {
|
||||
if (!document._id) {
|
||||
document._id = newid()
|
||||
}
|
||||
return this.put(document, opts)
|
||||
}
|
||||
|
||||
async put(document: AnyDocument, opts?: DatabasePutOpts) {
|
||||
if (!document._id) {
|
||||
throw new Error("Cannot store document without _id field.")
|
||||
|
@ -146,7 +178,7 @@ export class DatabaseImpl implements Database {
|
|||
|
||||
async destroy() {
|
||||
try {
|
||||
await DatabaseImpl.nano.db.destroy(this.name)
|
||||
await this.nano().db.destroy(this.name)
|
||||
} catch (err: any) {
|
||||
// didn't exist, don't worry
|
||||
if (err.statusCode === 404) {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import env from "../../environment"
|
||||
|
||||
export const getCouchInfo = () => {
|
||||
const urlInfo = getUrlInfo()
|
||||
export const getCouchInfo = (connection?: string) => {
|
||||
const urlInfo = getUrlInfo(connection)
|
||||
let username
|
||||
let password
|
||||
if (env.COUCH_DB_USERNAME) {
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,11 +1,12 @@
|
|||
import {
|
||||
Integration,
|
||||
DatasourceFieldType,
|
||||
QueryType,
|
||||
Document,
|
||||
Integration,
|
||||
IntegrationBase,
|
||||
QueryType,
|
||||
} from "@budibase/types"
|
||||
|
||||
const PouchDB = require("pouchdb")
|
||||
import { db as dbCore } from "@budibase/backend-core"
|
||||
import { DatabaseWithConnection } from "@budibase/backend-core/src/db"
|
||||
|
||||
interface CouchDBConfig {
|
||||
url: string
|
||||
|
@ -39,6 +40,15 @@ const SCHEMA: Integration = {
|
|||
update: {
|
||||
type: QueryType.JSON,
|
||||
},
|
||||
get: {
|
||||
type: QueryType.FIELDS,
|
||||
fields: {
|
||||
id: {
|
||||
type: DatasourceFieldType.STRING,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
delete: {
|
||||
type: QueryType.FIELDS,
|
||||
fields: {
|
||||
|
@ -57,7 +67,7 @@ class CouchDBIntegration implements IntegrationBase {
|
|||
|
||||
constructor(config: CouchDBConfig) {
|
||||
this.config = config
|
||||
this.client = new PouchDB(`${config.url}/${config.database}`)
|
||||
this.client = dbCore.DatabaseWithConnection(config.database, config.url)
|
||||
}
|
||||
|
||||
async query(
|
||||
|
@ -66,31 +76,48 @@ class CouchDBIntegration implements IntegrationBase {
|
|||
query: { json?: object; id?: string }
|
||||
) {
|
||||
try {
|
||||
const response = await this.client[command](query.id || query.json)
|
||||
await this.client.close()
|
||||
return response
|
||||
return await this.client[command](query.id || query.json)
|
||||
} catch (err) {
|
||||
console.error(errorMsg, err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async create(query: { json: object }) {
|
||||
return this.query("post", "Error writing to couchDB", query)
|
||||
private parse(query: { json: string | object }) {
|
||||
return typeof query.json === "string" ? JSON.parse(query.json) : query.json
|
||||
}
|
||||
|
||||
async read(query: { json: object }) {
|
||||
async create(query: { json: string | object }) {
|
||||
const parsed = this.parse(query)
|
||||
return this.query("post", "Error writing to couchDB", { json: parsed })
|
||||
}
|
||||
|
||||
async read(query: { json: string | object }) {
|
||||
const parsed = this.parse(query)
|
||||
const result = await this.query("allDocs", "Error querying couchDB", {
|
||||
json: {
|
||||
include_docs: true,
|
||||
...query.json,
|
||||
...parsed,
|
||||
},
|
||||
})
|
||||
return result.rows.map((row: { doc: object }) => row.doc)
|
||||
}
|
||||
|
||||
async update(query: { json: object }) {
|
||||
return this.query("put", "Error updating couchDB document", query)
|
||||
async update(query: { json: string | object }) {
|
||||
const parsed: Document = this.parse(query)
|
||||
if (!parsed?._rev && parsed?._id) {
|
||||
const oldDoc = await this.get({ id: parsed._id })
|
||||
parsed._rev = oldDoc._rev
|
||||
}
|
||||
return this.query("put", "Error updating couchDB document", {
|
||||
json: parsed,
|
||||
})
|
||||
}
|
||||
|
||||
async get(query: { id: string }) {
|
||||
return this.query("get", "Error retrieving couchDB document by ID", {
|
||||
id: query.id,
|
||||
})
|
||||
}
|
||||
|
||||
async delete(query: { id: string }) {
|
||||
|
|
|
@ -1,23 +1,32 @@
|
|||
jest.mock(
|
||||
"pouchdb",
|
||||
() =>
|
||||
function CouchDBMock(this: any) {
|
||||
this.post = jest.fn()
|
||||
this.allDocs = jest.fn(() => ({ rows: [] }))
|
||||
this.put = jest.fn()
|
||||
this.get = jest.fn()
|
||||
this.remove = jest.fn()
|
||||
this.plugin = jest.fn()
|
||||
this.close = jest.fn()
|
||||
}
|
||||
)
|
||||
import { DatabaseWithConnection } from "@budibase/backend-core/src/db"
|
||||
|
||||
jest.mock("@budibase/backend-core", () => {
|
||||
const core = jest.requireActual("@budibase/backend-core")
|
||||
return {
|
||||
...core,
|
||||
db: {
|
||||
...core.db,
|
||||
DatabaseWithConnection: function () {
|
||||
return {
|
||||
post: jest.fn(),
|
||||
allDocs: jest.fn().mockReturnValue({ rows: [] }),
|
||||
put: jest.fn(),
|
||||
get: jest.fn().mockReturnValue({ _rev: "a" }),
|
||||
remove: jest.fn(),
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
import { default as CouchDBIntegration } from "../couchdb"
|
||||
|
||||
class TestConfiguration {
|
||||
integration: any
|
||||
|
||||
constructor(config: any = {}) {
|
||||
constructor(
|
||||
config: any = { url: "http://somewhere", database: "something" }
|
||||
) {
|
||||
this.integration = new CouchDBIntegration.integration(config)
|
||||
}
|
||||
}
|
||||
|
@ -33,8 +42,8 @@ describe("CouchDB Integration", () => {
|
|||
const doc = {
|
||||
test: 1,
|
||||
}
|
||||
const response = await config.integration.create({
|
||||
json: doc,
|
||||
await config.integration.create({
|
||||
json: JSON.stringify(doc),
|
||||
})
|
||||
expect(config.integration.client.post).toHaveBeenCalledWith(doc)
|
||||
})
|
||||
|
@ -44,8 +53,8 @@ describe("CouchDB Integration", () => {
|
|||
name: "search",
|
||||
}
|
||||
|
||||
const response = await config.integration.read({
|
||||
json: doc,
|
||||
await config.integration.read({
|
||||
json: JSON.stringify(doc),
|
||||
})
|
||||
|
||||
expect(config.integration.client.allDocs).toHaveBeenCalledWith({
|
||||
|
@ -60,11 +69,14 @@ describe("CouchDB Integration", () => {
|
|||
name: "search",
|
||||
}
|
||||
|
||||
const response = await config.integration.update({
|
||||
json: doc,
|
||||
await config.integration.update({
|
||||
json: JSON.stringify(doc),
|
||||
})
|
||||
|
||||
expect(config.integration.client.put).toHaveBeenCalledWith(doc)
|
||||
expect(config.integration.client.put).toHaveBeenCalledWith({
|
||||
...doc,
|
||||
_rev: "a",
|
||||
})
|
||||
})
|
||||
|
||||
it("calls the delete method with the correct params", async () => {
|
||||
|
|
Loading…
Reference in a new issue