Updates to use our new Nano layer for CouchDB integration rather than PouchDB.

This commit is contained in:
mike12345567 2023-01-31 19:49:31 +00:00
parent 9f3cb5934b
commit f4379fcb4f
5 changed files with 124 additions and 1038 deletions

View File

@ -15,26 +15,10 @@ import { getCouchInfo } from "./connections"
import { directCouchCall } from "./utils" import { directCouchCall } from "./utils"
import { getPouchDB } from "./pouchDB" import { getPouchDB } from "./pouchDB"
import { WriteStream, ReadStream } from "fs" import { WriteStream, ReadStream } from "fs"
import { newid } from "../../newid"
export class DatabaseImpl implements Database { function buildNano(couchInfo: { url: string; cookie: string }) {
public readonly name: string return Nano({
private static nano: Nano.ServerScope
private readonly pouchOpts: DatabaseOpts
constructor(dbName?: string, opts?: DatabaseOpts) {
if (dbName == null) {
throw new Error("Database name cannot be undefined.")
}
this.name = dbName
this.pouchOpts = opts || {}
if (!DatabaseImpl.nano) {
DatabaseImpl.init()
}
}
static init() {
const couchInfo = getCouchInfo()
DatabaseImpl.nano = Nano({
url: couchInfo.url, url: couchInfo.url,
requestDefaults: { requestDefaults: {
headers: { headers: {
@ -45,11 +29,41 @@ export class DatabaseImpl implements Database {
}) })
} }
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, 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()
}
}
static init() {
const couchInfo = getCouchInfo()
DatabaseImpl.nano = buildNano(couchInfo)
}
async exists() { async exists() {
let response = await directCouchCall(`/${this.name}`, "HEAD") let response = await directCouchCall(`/${this.name}`, "HEAD")
return response.status === 200 return response.status === 200
} }
private nano() {
return this.instanceNano || DatabaseImpl.nano
}
async checkSetup() { async checkSetup() {
let shouldCreate = !this.pouchOpts?.skip_setup let shouldCreate = !this.pouchOpts?.skip_setup
// check exists in a lightweight fashion // check exists in a lightweight fashion
@ -58,9 +72,9 @@ export class DatabaseImpl implements Database {
throw new Error("DB does not exist") throw new Error("DB does not exist")
} }
if (!exists) { 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) { private async updateOutput(fnc: any) {
@ -101,6 +115,13 @@ export class DatabaseImpl implements Database {
return this.updateOutput(() => db.destroy(_id, _rev)) 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) { async put(document: AnyDocument, opts?: DatabasePutOpts) {
if (!document._id) { if (!document._id) {
throw new Error("Cannot store document without _id field.") throw new Error("Cannot store document without _id field.")
@ -146,7 +167,7 @@ export class DatabaseImpl implements Database {
async destroy() { async destroy() {
try { try {
await DatabaseImpl.nano.db.destroy(this.name) await this.nano().db.destroy(this.name)
} catch (err: any) { } catch (err: any) {
// didn't exist, don't worry // didn't exist, don't worry
if (err.statusCode === 404) { if (err.statusCode === 404) {

View File

@ -1,7 +1,7 @@
import env from "../../environment" import env from "../../environment"
export const getCouchInfo = () => { export const getCouchInfo = (connection?: string) => {
const urlInfo = getUrlInfo() const urlInfo = getUrlInfo(connection)
let username let username
let password let password
if (env.COUCH_DB_USERNAME) { if (env.COUCH_DB_USERNAME) {

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +1,11 @@
import { import {
Integration,
DatasourceFieldType, DatasourceFieldType,
QueryType, Document,
Integration,
IntegrationBase, IntegrationBase,
QueryType,
} from "@budibase/types" } from "@budibase/types"
import { db as dbCore } from "@budibase/backend-core"
const PouchDB = require("pouchdb")
interface CouchDBConfig { interface CouchDBConfig {
url: string url: string
@ -39,6 +39,15 @@ const SCHEMA: Integration = {
update: { update: {
type: QueryType.JSON, type: QueryType.JSON,
}, },
get: {
type: QueryType.FIELDS,
fields: {
id: {
type: DatasourceFieldType.STRING,
required: true,
},
},
},
delete: { delete: {
type: QueryType.FIELDS, type: QueryType.FIELDS,
fields: { fields: {
@ -57,7 +66,11 @@ class CouchDBIntegration implements IntegrationBase {
constructor(config: CouchDBConfig) { constructor(config: CouchDBConfig) {
this.config = config this.config = config
this.client = new PouchDB(`${config.url}/${config.database}`) this.client = new dbCore.DatabaseImpl(
config.database,
undefined,
config.url
)
} }
async query( async query(
@ -66,31 +79,48 @@ class CouchDBIntegration implements IntegrationBase {
query: { json?: object; id?: string } query: { json?: object; id?: string }
) { ) {
try { try {
const response = await this.client[command](query.id || query.json) return await this.client[command](query.id || query.json)
await this.client.close()
return response
} catch (err) { } catch (err) {
console.error(errorMsg, err) console.error(errorMsg, err)
throw err throw err
} }
} }
async create(query: { json: object }) { private parse(query: { json: string | object }) {
return this.query("post", "Error writing to couchDB", query) 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", { const result = await this.query("allDocs", "Error querying couchDB", {
json: { json: {
include_docs: true, include_docs: true,
...query.json, ...parsed,
}, },
}) })
return result.rows.map((row: { doc: object }) => row.doc) return result.rows.map((row: { doc: object }) => row.doc)
} }
async update(query: { json: object }) { async update(query: { json: string | object }) {
return this.query("put", "Error updating couchDB document", query) 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 }) { async delete(query: { id: string }) {

View File

@ -1,16 +1,19 @@
jest.mock( jest.mock("@budibase/backend-core", () => {
"pouchdb", const core = jest.requireActual("@budibase/backend-core")
() => return {
function CouchDBMock(this: any) { ...core,
db: {
...core.db,
DatabaseImpl: function () {
this.post = jest.fn() this.post = jest.fn()
this.allDocs = jest.fn(() => ({ rows: [] })) this.allDocs = jest.fn().mockReturnValue({ rows: [] })
this.put = jest.fn() this.put = jest.fn()
this.get = jest.fn() this.get = jest.fn().mockReturnValue({ _rev: "a" })
this.remove = jest.fn() this.remove = jest.fn()
this.plugin = jest.fn() },
this.close = jest.fn() },
} }
) })
import { default as CouchDBIntegration } from "../couchdb" import { default as CouchDBIntegration } from "../couchdb"
@ -33,8 +36,8 @@ describe("CouchDB Integration", () => {
const doc = { const doc = {
test: 1, test: 1,
} }
const response = await config.integration.create({ await config.integration.create({
json: doc, json: JSON.stringify(doc),
}) })
expect(config.integration.client.post).toHaveBeenCalledWith(doc) expect(config.integration.client.post).toHaveBeenCalledWith(doc)
}) })
@ -44,8 +47,8 @@ describe("CouchDB Integration", () => {
name: "search", name: "search",
} }
const response = await config.integration.read({ await config.integration.read({
json: doc, json: JSON.stringify(doc),
}) })
expect(config.integration.client.allDocs).toHaveBeenCalledWith({ expect(config.integration.client.allDocs).toHaveBeenCalledWith({
@ -60,11 +63,14 @@ describe("CouchDB Integration", () => {
name: "search", name: "search",
} }
const response = await config.integration.update({ await config.integration.update({
json: doc, 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 () => { it("calls the delete method with the correct params", async () => {