diff --git a/packages/server/src/api/controllers/row.js b/packages/server/src/api/controllers/row.js index 8e33be1f22..6a57d54bea 100644 --- a/packages/server/src/api/controllers/row.js +++ b/packages/server/src/api/controllers/row.js @@ -9,9 +9,7 @@ const { ViewNames, } = require("../../db/utils") const usersController = require("./user") -const { coerceRowValues } = require("../../utilities") -const env = require("../../environment") -const { OBJ_STORE_DIRECTORY } = require("../../constants") +const { coerceRowValues, enrichRows } = require("../../utilities") const TABLE_VIEW_BEGINS_WITH = `all${SEPARATOR}${DocumentTypes.TABLE}${SEPARATOR}` @@ -53,32 +51,6 @@ async function findRow(db, appId, tableId, rowId) { return row } -/** - * This function "enriches" the input rows with anything they are supposed to contain, for example - * link records or attachment links. - */ -async function enrichRows(appId, table, rows) { - // attach any linked row information - const enriched = await linkRows.attachLinkInfo(appId, rows) - // update the attachments URL depending on hosting - if (env.CLOUD && env.SELF_HOSTED) { - for (let [property, column] of Object.entries(table.schema)) { - if (column.type === "attachment") { - for (let row of enriched) { - if (row[property] == null || row[property].length === 0) { - continue - } - row[property].forEach(attachment => { - attachment.url = `${OBJ_STORE_DIRECTORY}/${appId}/${attachment.url}` - attachment.url = attachment.url.replace("//", "/") - }) - } - } - } - } - return enriched -} - exports.patch = async function(ctx) { const appId = ctx.user.appId const db = new CouchDB(appId) @@ -249,12 +221,12 @@ exports.fetchTableRows = async function(ctx) { const db = new CouchDB(appId) // special case for users, fetch through the user controller - let rows, table = await db.get(ctx.params.tableId) + let rows, + table = await db.get(ctx.params.tableId) if (ctx.params.tableId === ViewNames.USERS) { await usersController.fetch(ctx) rows = ctx.body } else { - const response = await db.allDocs( getRowParams(ctx.params.tableId, null, { include_docs: true, @@ -359,7 +331,7 @@ exports.fetchEnrichedRow = async function(ctx) { let linkedRows = await enrichRows( appId, table, - response.rows.map(row => row.doc), + response.rows.map(row => row.doc) ) // insert the link rows in the correct place throughout the main row for (let fieldName of Object.keys(table.schema)) { diff --git a/packages/server/src/api/controllers/table.js b/packages/server/src/api/controllers/table.js index 488809d156..1dd53e2277 100644 --- a/packages/server/src/api/controllers/table.js +++ b/packages/server/src/api/controllers/table.js @@ -108,7 +108,7 @@ exports.save = async function(ctx) { } // update linked rows - await linkRows.updateLinks({ + const linkResp = await linkRows.updateLinks({ appId, eventType: oldTable ? linkRows.EventType.TABLE_UPDATED @@ -116,6 +116,9 @@ exports.save = async function(ctx) { table: tableToSave, oldTable: oldTable, }) + if (linkResp != null && linkResp._rev) { + tableToSave._rev = linkResp._rev + } // don't perform any updates until relationships have been // checked by the updateLinks function diff --git a/packages/server/src/api/routes/tests/couchTestUtils.js b/packages/server/src/api/routes/tests/couchTestUtils.js index 463d88e637..adf75555ff 100644 --- a/packages/server/src/api/routes/tests/couchTestUtils.js +++ b/packages/server/src/api/routes/tests/couchTestUtils.js @@ -4,6 +4,7 @@ const { BUILTIN_ROLE_IDS } = require("../../../utilities/security/roles") const packageJson = require("../../../../package") const jwt = require("jsonwebtoken") const env = require("../../../environment") +const { cloneDeep } = require("lodash/fp") const TEST_CLIENT_ID = "test-client-id" @@ -37,29 +38,28 @@ exports.defaultHeaders = appId => { return headers } -exports.createTable = async (request, appId, table) => { - if (table != null && table._id) { - delete table._id - } - table = table || { - name: "TestTable", - type: "table", - key: "name", - schema: { - name: { +exports.BASE_TABLE = { + name: "TestTable", + type: "table", + key: "name", + schema: { + name: { + type: "string", + constraints: { type: "string", - constraints: { - type: "string", - }, - }, - description: { - type: "string", - constraints: { - type: "string", - }, }, }, - } + description: { + type: "string", + constraints: { + type: "string", + }, + }, + }, +} + +exports.createTable = async (request, appId, table) => { + table = table || exports.BASE_TABLE const res = await request .post(`/api/tables`) @@ -68,6 +68,25 @@ exports.createTable = async (request, appId, table) => { return res.body } +exports.createLinkedTable = async (request, appId) => { + // get the ID to link to + const table = await exports.createTable(request, appId) + table.schema.link = { + type: "link", + fieldName: "link", + tableId: table._id, + } + return exports.createTable(request, appId, table) +} + +exports.createAttachmentTable = async (request, appId) => { + const table = await exports.createTable(request, appId) + table.schema.attachment = { + type: "attachment", + } + return exports.createTable(request, appId, table) +} + exports.getAllFromTable = async (request, appId, tableId) => { const res = await request .get(`/api/${tableId}/rows`) diff --git a/packages/server/src/api/routes/tests/row.spec.js b/packages/server/src/api/routes/tests/row.spec.js index 0698250050..47977ed207 100644 --- a/packages/server/src/api/routes/tests/row.spec.js +++ b/packages/server/src/api/routes/tests/row.spec.js @@ -3,7 +3,11 @@ const { createTable, supertest, defaultHeaders, + createLinkedTable, + createAttachmentTable, } = require("./couchTestUtils"); +const { enrichRows } = require("../../../utilities") +const env = require("../../../environment") describe("/rows", () => { let request @@ -270,4 +274,44 @@ describe("/rows", () => { }) }) + + describe("enrich row unit test", () => { + it("should allow enriching some linked rows", async () => { + const table = await createLinkedTable(request, appId) + const firstRow = (await createRow({ + name: "Test Contact", + description: "original description", + tableId: table._id + })).body + const secondRow = (await createRow({ + name: "Test 2", + description: "og desc", + link: [firstRow._id], + tableId: table._id, + })).body + const enriched = await enrichRows(appId, table, [secondRow]) + expect(enriched[0].link.length).toBe(1) + expect(enriched[0].link[0]).toBe(firstRow._id) + }) + }) + + it("should allow enriching attachment rows", async () => { + const table = await createAttachmentTable(request, appId) + const row = (await createRow({ + name: "test", + description: "test", + attachment: [{ + url: "/test/thing", + }], + tableId: table._id, + })).body + // the environment needs configured for this + env.CLOUD = 1 + env.SELF_HOSTED = 1 + const enriched = await enrichRows(appId, table, [row]) + expect(enriched[0].attachment[0].url).toBe(`/app-assets/assets/${appId}/test/thing`) + // remove env config + env.CLOUD = undefined + env.SELF_HOSTED = undefined + }) }) \ No newline at end of file diff --git a/packages/server/src/db/linkedRows/LinkController.js b/packages/server/src/db/linkedRows/LinkController.js index d6116b76f6..061a9ac1ae 100644 --- a/packages/server/src/db/linkedRows/LinkController.js +++ b/packages/server/src/db/linkedRows/LinkController.js @@ -252,7 +252,11 @@ class LinkController { tableId: table._id, fieldName: fieldName, } - await this._db.put(linkedTable) + const response = await this._db.put(linkedTable) + // special case for when linking back to self, make sure rev updated + if (linkedTable._id === table._id) { + table._rev = response.rev + } } } return table diff --git a/packages/server/src/db/linkedRows/linkUtils.js b/packages/server/src/db/linkedRows/linkUtils.js index 5f9dca4088..6549f9b61b 100644 --- a/packages/server/src/db/linkedRows/linkUtils.js +++ b/packages/server/src/db/linkedRows/linkUtils.js @@ -31,11 +31,14 @@ exports.createLinkView = async appId => { thisId: doc1.rowId, fieldName: doc1.fieldName, }) - emit([doc2.tableId, doc2.rowId], { - id: doc1.rowId, - thisId: doc2.rowId, - fieldName: doc2.fieldName, - }) + // if linking to same table can't emit twice + if (doc1.tableId !== doc2.tableId) { + emit([doc2.tableId, doc2.rowId], { + id: doc1.rowId, + thisId: doc2.rowId, + fieldName: doc2.fieldName, + }) + } } }.toString(), } diff --git a/packages/server/src/utilities/index.js b/packages/server/src/utilities/index.js index 9da2937a4d..31cc74b5e6 100644 --- a/packages/server/src/utilities/index.js +++ b/packages/server/src/utilities/index.js @@ -3,6 +3,8 @@ const { DocumentTypes, SEPARATOR } = require("../db/utils") const fs = require("fs") const { cloneDeep } = require("lodash/fp") const CouchDB = require("../db") +const { OBJ_STORE_DIRECTORY } = require("../constants") +const linkRows = require("../db/linkedRows") const APP_PREFIX = DocumentTypes.APP + SEPARATOR @@ -211,3 +213,34 @@ exports.getAllApps = async () => { .map(({ value }) => value) } } + +/** + * This function "enriches" the input rows with anything they are supposed to contain, for example + * link records or attachment links. + * @param {string} appId the ID of the application for which rows are being enriched. + * @param {object} table the table from which these rows came from originally, this is used to determine + * the schema of the rows and then enrich. + * @param {object[]} rows the rows which are to be enriched. + * @returns {object[]} the enriched rows will be returned. + */ +exports.enrichRows = async (appId, table, rows) => { + // attach any linked row information + const enriched = await linkRows.attachLinkInfo(appId, rows) + // update the attachments URL depending on hosting + if (env.CLOUD && env.SELF_HOSTED) { + for (let [property, column] of Object.entries(table.schema)) { + if (column.type === "attachment") { + for (let row of enriched) { + if (row[property] == null || row[property].length === 0) { + continue + } + row[property].forEach(attachment => { + attachment.url = `${OBJ_STORE_DIRECTORY}/${appId}/${attachment.url}` + attachment.url = attachment.url.replace("//", "/") + }) + } + } + } + } + return enriched +}