Merge pull request #1296 from Budibase/tests/relationships
Server relationship tests
This commit is contained in:
commit
2326095dd4
|
@ -59,6 +59,7 @@
|
||||||
"!src/db/dynamoClient.js",
|
"!src/db/dynamoClient.js",
|
||||||
"!src/utilities/usageQuota.js",
|
"!src/utilities/usageQuota.js",
|
||||||
"!src/api/routes/tests/**/*",
|
"!src/api/routes/tests/**/*",
|
||||||
|
"!src/db/tests/**/*",
|
||||||
"!src/tests/**/*",
|
"!src/tests/**/*",
|
||||||
"!src/automations/tests/**/*",
|
"!src/automations/tests/**/*",
|
||||||
"!src/utilities/fileProcessor.js",
|
"!src/utilities/fileProcessor.js",
|
||||||
|
|
|
@ -133,12 +133,19 @@ class LinkController {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns whether the two schemas are equal (in the important parts, not a pure equality check)
|
* Returns whether the two link schemas are equal (in the important parts, not a pure equality check)
|
||||||
*/
|
*/
|
||||||
areSchemasEqual(schema1, schema2) {
|
areLinkSchemasEqual(linkSchema1, linkSchema2) {
|
||||||
const compareFields = ["name", "type", "tableId", "fieldName", "autocolumn"]
|
const compareFields = [
|
||||||
|
"name",
|
||||||
|
"type",
|
||||||
|
"tableId",
|
||||||
|
"fieldName",
|
||||||
|
"autocolumn",
|
||||||
|
"relationshipType",
|
||||||
|
]
|
||||||
for (let field of compareFields) {
|
for (let field of compareFields) {
|
||||||
if (schema1[field] !== schema2[field]) {
|
if (linkSchema1[field] !== linkSchema2[field]) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -146,24 +153,24 @@ class LinkController {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given two the field of this table, and the field of the linked table, this makes sure
|
* Given the link field of this table, and the link field of the linked table, this makes sure
|
||||||
* the state of relationship type is accurate on both.
|
* the state of relationship type is accurate on both.
|
||||||
*/
|
*/
|
||||||
handleRelationshipType(field, linkedField) {
|
handleRelationshipType(linkerField, linkedField) {
|
||||||
if (
|
if (
|
||||||
!field.relationshipType ||
|
!linkerField.relationshipType ||
|
||||||
field.relationshipType === RelationshipTypes.MANY_TO_MANY
|
linkerField.relationshipType === RelationshipTypes.MANY_TO_MANY
|
||||||
) {
|
) {
|
||||||
linkedField.relationshipType = RelationshipTypes.MANY_TO_MANY
|
linkedField.relationshipType = RelationshipTypes.MANY_TO_MANY
|
||||||
// make sure by default all are many to many (if not specified)
|
// make sure by default all are many to many (if not specified)
|
||||||
field.relationshipType = RelationshipTypes.MANY_TO_MANY
|
linkerField.relationshipType = RelationshipTypes.MANY_TO_MANY
|
||||||
} else if (field.relationshipType === RelationshipTypes.MANY_TO_ONE) {
|
} else if (linkerField.relationshipType === RelationshipTypes.MANY_TO_ONE) {
|
||||||
// Ensure that the other side of the relationship is locked to one record
|
// Ensure that the other side of the relationship is locked to one record
|
||||||
linkedField.relationshipType = RelationshipTypes.ONE_TO_MANY
|
linkedField.relationshipType = RelationshipTypes.ONE_TO_MANY
|
||||||
} else if (field.relationshipType === RelationshipTypes.ONE_TO_MANY) {
|
} else if (linkerField.relationshipType === RelationshipTypes.ONE_TO_MANY) {
|
||||||
linkedField.relationshipType = RelationshipTypes.MANY_TO_ONE
|
linkedField.relationshipType = RelationshipTypes.MANY_TO_ONE
|
||||||
}
|
}
|
||||||
return { field, linkedField }
|
return { linkerField, linkedField }
|
||||||
}
|
}
|
||||||
|
|
||||||
// all operations here will assume that the table
|
// all operations here will assume that the table
|
||||||
|
@ -336,6 +343,7 @@ class LinkController {
|
||||||
try {
|
try {
|
||||||
linkedTable = await this._db.get(field.tableId)
|
linkedTable = await this._db.get(field.tableId)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
/* istanbul ignore next */
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const fields = this.handleRelationshipType(field, {
|
const fields = this.handleRelationshipType(field, {
|
||||||
|
@ -347,7 +355,7 @@ class LinkController {
|
||||||
})
|
})
|
||||||
|
|
||||||
// update table schema after checking relationship types
|
// update table schema after checking relationship types
|
||||||
schema[fieldName] = fields.field
|
schema[fieldName] = fields.linkerField
|
||||||
const linkedField = fields.linkedField
|
const linkedField = fields.linkedField
|
||||||
|
|
||||||
if (field.autocolumn) {
|
if (field.autocolumn) {
|
||||||
|
@ -358,7 +366,7 @@ class LinkController {
|
||||||
const existingSchema = linkedTable.schema[field.fieldName]
|
const existingSchema = linkedTable.schema[field.fieldName]
|
||||||
if (
|
if (
|
||||||
existingSchema != null &&
|
existingSchema != null &&
|
||||||
!this.areSchemasEqual(existingSchema, linkedField)
|
!this.areLinkSchemasEqual(existingSchema, linkedField)
|
||||||
) {
|
) {
|
||||||
throw new Error("Cannot overwrite existing column.")
|
throw new Error("Cannot overwrite existing column.")
|
||||||
}
|
}
|
||||||
|
@ -416,6 +424,7 @@ class LinkController {
|
||||||
await this._db.put(linkedTable)
|
await this._db.put(linkedTable)
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
/* istanbul ignore next */
|
||||||
Sentry.captureException(err)
|
Sentry.captureException(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -75,6 +75,7 @@ exports.getLinkDocuments = async function({
|
||||||
await exports.createLinkView(appId)
|
await exports.createLinkView(appId)
|
||||||
return exports.getLinkDocuments(arguments[0])
|
return exports.getLinkDocuments(arguments[0])
|
||||||
} else {
|
} else {
|
||||||
|
/* istanbul ignore next */
|
||||||
Sentry.captureException(err)
|
Sentry.captureException(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,218 @@
|
||||||
|
const TestConfig = require("../../tests/utilities/TestConfiguration")
|
||||||
|
const { basicRow, basicLinkedRow, basicTable } = require("../../tests/utilities/structures")
|
||||||
|
const LinkController = require("../linkedRows/LinkController")
|
||||||
|
const { RelationshipTypes } = require("../../constants")
|
||||||
|
const { cloneDeep } = require("lodash/fp")
|
||||||
|
|
||||||
|
describe("test the link controller", () => {
|
||||||
|
let config = new TestConfig(false)
|
||||||
|
let table1, table2
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await config.init()
|
||||||
|
const { _id } = await config.createTable()
|
||||||
|
table2 = await config.createLinkedTable(RelationshipTypes.MANY_TO_MANY, ["link", "link2"])
|
||||||
|
// update table after creating link
|
||||||
|
table1 = await config.getTable(_id)
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(config.end)
|
||||||
|
|
||||||
|
function createLinkController(table, row = null, oldTable = null) {
|
||||||
|
const linkConfig = {
|
||||||
|
appId: config.getAppId(),
|
||||||
|
tableId: table._id,
|
||||||
|
table,
|
||||||
|
}
|
||||||
|
if (row) {
|
||||||
|
linkConfig.row = row
|
||||||
|
}
|
||||||
|
if (oldTable) {
|
||||||
|
linkConfig.oldTable = oldTable
|
||||||
|
}
|
||||||
|
return new LinkController(linkConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createLinkedRow(linkField = "link", t1 = table1, t2 = table2) {
|
||||||
|
const row = await config.createRow(basicRow(t2._id))
|
||||||
|
const { _id } = await config.createRow(basicLinkedRow(t1._id, row._id, linkField))
|
||||||
|
return config.getRow(t1._id, _id)
|
||||||
|
}
|
||||||
|
|
||||||
|
it("should be able to confirm if two table schemas are equal", () => {
|
||||||
|
const controller = createLinkController(table1)
|
||||||
|
let equal = controller.areLinkSchemasEqual(table2.schema.link, table2.schema.link)
|
||||||
|
expect(equal).toEqual(true)
|
||||||
|
equal = controller.areLinkSchemasEqual(table1.schema.link, table2.schema.link)
|
||||||
|
expect(equal).toEqual(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should be able to check the relationship types across two fields", () => {
|
||||||
|
const controller = createLinkController(table1)
|
||||||
|
// empty case
|
||||||
|
let output = controller.handleRelationshipType({}, {})
|
||||||
|
expect(output.linkedField.relationshipType).toEqual(RelationshipTypes.MANY_TO_MANY)
|
||||||
|
expect(output.linkerField.relationshipType).toEqual(RelationshipTypes.MANY_TO_MANY)
|
||||||
|
output = controller.handleRelationshipType({ relationshipType: RelationshipTypes.MANY_TO_MANY }, {})
|
||||||
|
expect(output.linkedField.relationshipType).toEqual(RelationshipTypes.MANY_TO_MANY)
|
||||||
|
expect(output.linkerField.relationshipType).toEqual(RelationshipTypes.MANY_TO_MANY)
|
||||||
|
output = controller.handleRelationshipType({ relationshipType: RelationshipTypes.MANY_TO_ONE }, {})
|
||||||
|
expect(output.linkedField.relationshipType).toEqual(RelationshipTypes.ONE_TO_MANY)
|
||||||
|
expect(output.linkerField.relationshipType).toEqual(RelationshipTypes.MANY_TO_ONE)
|
||||||
|
output = controller.handleRelationshipType({ relationshipType: RelationshipTypes.ONE_TO_MANY }, {})
|
||||||
|
expect(output.linkedField.relationshipType).toEqual(RelationshipTypes.MANY_TO_ONE)
|
||||||
|
expect(output.linkerField.relationshipType).toEqual(RelationshipTypes.ONE_TO_MANY)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should be able to delete a row", async () => {
|
||||||
|
const row = await createLinkedRow()
|
||||||
|
const controller = createLinkController(table1, row)
|
||||||
|
// get initial count
|
||||||
|
const beforeLinks = await controller.getRowLinkDocs(row._id)
|
||||||
|
await controller.rowDeleted()
|
||||||
|
let afterLinks = await controller.getRowLinkDocs(row._id)
|
||||||
|
expect(beforeLinks.length).toEqual(1)
|
||||||
|
expect(afterLinks.length).toEqual(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("shouldn't throw an error when deleting a row with no links", async () => {
|
||||||
|
const row = await config.createRow(basicRow(table1._id))
|
||||||
|
const controller = createLinkController(table1, row)
|
||||||
|
let error
|
||||||
|
try {
|
||||||
|
await controller.rowDeleted()
|
||||||
|
} catch (err) {
|
||||||
|
error = err
|
||||||
|
}
|
||||||
|
expect(error).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should throw an error when validating a table which is invalid", () => {
|
||||||
|
const controller = createLinkController(table1)
|
||||||
|
const copyTable = {
|
||||||
|
...table1
|
||||||
|
}
|
||||||
|
copyTable.schema.otherTableLink = {
|
||||||
|
type: "link",
|
||||||
|
fieldName: "link",
|
||||||
|
tableId: table2._id,
|
||||||
|
}
|
||||||
|
let error
|
||||||
|
try {
|
||||||
|
controller.validateTable(copyTable)
|
||||||
|
} catch (err) {
|
||||||
|
error = err
|
||||||
|
}
|
||||||
|
expect(error).toBeDefined()
|
||||||
|
expect(error.message).toEqual("Cannot re-use the linked column name for a linked table.")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should be able to remove a link when saving/updating the row", async () => {
|
||||||
|
const row = await createLinkedRow()
|
||||||
|
// remove the link from the row
|
||||||
|
row.link = []
|
||||||
|
const controller = createLinkController(table1, row)
|
||||||
|
await controller.rowSaved()
|
||||||
|
let links = await controller.getRowLinkDocs(row._id)
|
||||||
|
expect(links.length).toEqual(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should be able to delete a table and have links deleted", async () => {
|
||||||
|
await createLinkedRow()
|
||||||
|
const controller = createLinkController(table1)
|
||||||
|
let before = await controller.getTableLinkDocs()
|
||||||
|
await controller.tableDeleted()
|
||||||
|
let after = await controller.getTableLinkDocs()
|
||||||
|
expect(before.length).toEqual(1)
|
||||||
|
expect(after.length).toEqual(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should be able to remove a linked field from a table", async () => {
|
||||||
|
await createLinkedRow()
|
||||||
|
await createLinkedRow("link2")
|
||||||
|
const controller = createLinkController(table1, null, table1)
|
||||||
|
let before = await controller.getTableLinkDocs()
|
||||||
|
await controller.removeFieldFromTable("link")
|
||||||
|
let after = await controller.getTableLinkDocs()
|
||||||
|
expect(before.length).toEqual(2)
|
||||||
|
// shouldn't delete the other field
|
||||||
|
expect(after.length).toEqual(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should throw an error when overwriting a link column", async () => {
|
||||||
|
const update = cloneDeep(table1)
|
||||||
|
update.schema.link.relationshipType = RelationshipTypes.MANY_TO_ONE
|
||||||
|
let error
|
||||||
|
try {
|
||||||
|
const controller = createLinkController(update)
|
||||||
|
await controller.tableSaved()
|
||||||
|
} catch (err) {
|
||||||
|
error = err
|
||||||
|
}
|
||||||
|
expect(error).toBeDefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should be able to remove a field view table update", async () => {
|
||||||
|
await createLinkedRow()
|
||||||
|
await createLinkedRow()
|
||||||
|
const newTable = cloneDeep(table1)
|
||||||
|
delete newTable.schema.link
|
||||||
|
const controller = createLinkController(newTable, null, table1)
|
||||||
|
await controller.tableUpdated()
|
||||||
|
const links = await controller.getTableLinkDocs()
|
||||||
|
expect(links.length).toEqual(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("shouldn't allow one to many having many relationships against it", async () => {
|
||||||
|
const firstTable = await config.createTable()
|
||||||
|
const { _id } = await config.createLinkedTable(RelationshipTypes.MANY_TO_ONE, ["link"])
|
||||||
|
const linkTable = await config.getTable(_id)
|
||||||
|
// an initial row to link around
|
||||||
|
const row = await createLinkedRow("link", linkTable, firstTable)
|
||||||
|
let error
|
||||||
|
try {
|
||||||
|
// create another row to initiate the error
|
||||||
|
await config.createRow(basicLinkedRow(row.tableId, row.link[0]))
|
||||||
|
} catch (err) {
|
||||||
|
error = err
|
||||||
|
}
|
||||||
|
expect(error).toBeDefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should not error if a link being created doesn't exist", async () => {
|
||||||
|
let error
|
||||||
|
try {
|
||||||
|
await config.createRow(basicLinkedRow(table1._id, "invalid"))
|
||||||
|
} catch (err) {
|
||||||
|
error = err
|
||||||
|
}
|
||||||
|
expect(error).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("make sure auto column goes onto other row too", async () => {
|
||||||
|
const table = await config.createTable()
|
||||||
|
const tableCfg = basicTable()
|
||||||
|
tableCfg.schema.link = {
|
||||||
|
type: "link",
|
||||||
|
fieldName: "link",
|
||||||
|
tableId: table._id,
|
||||||
|
name: "link",
|
||||||
|
autocolumn: true,
|
||||||
|
}
|
||||||
|
await config.createTable(tableCfg)
|
||||||
|
const afterTable = await config.getTable(table._id)
|
||||||
|
expect(afterTable.schema.link.autocolumn).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should be able to link to self", async () => {
|
||||||
|
const table = await config.createTable()
|
||||||
|
table.schema.link = {
|
||||||
|
type: "link",
|
||||||
|
fieldName: "link",
|
||||||
|
tableId: table._id,
|
||||||
|
name: "link",
|
||||||
|
autocolumn: true,
|
||||||
|
}
|
||||||
|
await config.updateTable(table)
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,74 @@
|
||||||
|
const TestConfig = require("../../tests/utilities/TestConfiguration")
|
||||||
|
const { basicTable, basicLinkedRow } = require("../../tests/utilities/structures")
|
||||||
|
const linkUtils = require("../linkedRows/linkUtils")
|
||||||
|
const links = require("../linkedRows")
|
||||||
|
const CouchDB = require("../index")
|
||||||
|
|
||||||
|
describe("test link functionality", () => {
|
||||||
|
const config = new TestConfig(false)
|
||||||
|
|
||||||
|
describe("getLinkedTable", () => {
|
||||||
|
let db, table
|
||||||
|
beforeEach(async () => {
|
||||||
|
await config.init()
|
||||||
|
db = new CouchDB(config.getAppId())
|
||||||
|
table = await config.createTable()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should be able to retrieve a linked table from a list", async () => {
|
||||||
|
const retrieved = await linkUtils.getLinkedTable(db, table._id, [table])
|
||||||
|
expect(retrieved._id).toBe(table._id)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should be able to retrieve a table from DB and update list", async () => {
|
||||||
|
const tables = []
|
||||||
|
const retrieved = await linkUtils.getLinkedTable(db, table._id, tables)
|
||||||
|
expect(retrieved._id).toBe(table._id)
|
||||||
|
expect(tables[0]).toBeDefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("getRelatedTableForField", () => {
|
||||||
|
let link = basicTable()
|
||||||
|
link.schema.link = {
|
||||||
|
fieldName: "otherLink",
|
||||||
|
tableId: "tableID",
|
||||||
|
type: "link",
|
||||||
|
}
|
||||||
|
|
||||||
|
it("should get the field from the table directly", () => {
|
||||||
|
expect(linkUtils.getRelatedTableForField(link, "link")).toBe("tableID")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should get the field from the link", () => {
|
||||||
|
expect(linkUtils.getRelatedTableForField(link, "otherLink")).toBe("tableID")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("getLinkDocuments", () => {
|
||||||
|
it("should create the link view when it doesn't exist", async () => {
|
||||||
|
// create the DB and a very basic app design DB
|
||||||
|
const db = new CouchDB("test")
|
||||||
|
await db.put({ _id: "_design/database", views: {} })
|
||||||
|
const output = await linkUtils.getLinkDocuments({
|
||||||
|
appId: "test",
|
||||||
|
tableId: "test",
|
||||||
|
rowId: "test",
|
||||||
|
includeDocs: false,
|
||||||
|
})
|
||||||
|
expect(Array.isArray(output)).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("attachLinkIDs", () => {
|
||||||
|
it("should be able to attach linkIDs", async () => {
|
||||||
|
await config.init()
|
||||||
|
await config.createTable()
|
||||||
|
const table = await config.createLinkedTable()
|
||||||
|
const row = await config.createRow()
|
||||||
|
const linkRow = await config.createRow(basicLinkedRow(table._id, row._id))
|
||||||
|
const attached = await links.attachLinkIDs(config.getAppId(), [linkRow])
|
||||||
|
expect(attached[0].link[0]).toBe(row._id)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -135,16 +135,22 @@ class TestConfiguration {
|
||||||
return this._req(null, { id: tableId }, controllers.table.find)
|
return this._req(null, { id: tableId }, controllers.table.find)
|
||||||
}
|
}
|
||||||
|
|
||||||
async createLinkedTable() {
|
async createLinkedTable(relationshipType = null, links = ["link"]) {
|
||||||
if (!this.table) {
|
if (!this.table) {
|
||||||
throw "Must have created a table first."
|
throw "Must have created a table first."
|
||||||
}
|
}
|
||||||
const tableConfig = basicTable()
|
const tableConfig = basicTable()
|
||||||
tableConfig.primaryDisplay = "name"
|
tableConfig.primaryDisplay = "name"
|
||||||
tableConfig.schema.link = {
|
for (let link of links) {
|
||||||
|
tableConfig.schema[link] = {
|
||||||
type: "link",
|
type: "link",
|
||||||
fieldName: "link",
|
fieldName: link,
|
||||||
tableId: this.table._id,
|
tableId: this.table._id,
|
||||||
|
name: link,
|
||||||
|
}
|
||||||
|
if (relationshipType) {
|
||||||
|
tableConfig.schema[link].relationshipType = relationshipType
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const linkedTable = await this.createTable(tableConfig)
|
const linkedTable = await this.createTable(tableConfig)
|
||||||
this.linkedTable = linkedTable
|
this.linkedTable = linkedTable
|
||||||
|
@ -163,8 +169,9 @@ class TestConfiguration {
|
||||||
if (!this.table) {
|
if (!this.table) {
|
||||||
throw "Test requires table to be configured."
|
throw "Test requires table to be configured."
|
||||||
}
|
}
|
||||||
config = config || basicRow(this.table._id)
|
const tableId = (config && config.tableId) || this.table._id
|
||||||
return this._req(config, { tableId: this.table._id }, controllers.row.save)
|
config = config || basicRow(tableId)
|
||||||
|
return this._req(config, { tableId }, controllers.row.save)
|
||||||
}
|
}
|
||||||
|
|
||||||
async getRow(tableId, rowId) {
|
async getRow(tableId, rowId) {
|
||||||
|
|
|
@ -53,6 +53,14 @@ exports.basicRow = tableId => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.basicLinkedRow = (tableId, linkedRowId, linkField = "link") => {
|
||||||
|
// this is based on the basic linked tables you get from the test configuration
|
||||||
|
return {
|
||||||
|
...exports.basicRow(tableId),
|
||||||
|
[linkField]: [linkedRowId],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
exports.basicRole = () => {
|
exports.basicRole = () => {
|
||||||
return {
|
return {
|
||||||
name: "NewRole",
|
name: "NewRole",
|
||||||
|
|
Loading…
Reference in New Issue