diff --git a/packages/server/src/api/controllers/row/external.js b/packages/server/src/api/controllers/row/external.js index 896f5a78e2..79a347750f 100644 --- a/packages/server/src/api/controllers/row/external.js +++ b/packages/server/src/api/controllers/row/external.js @@ -1,6 +1,6 @@ const { makeExternalQuery } = require("./utils") -const { DataSourceOperation, SortDirection } = require("../../../constants") -const { getExternalTable } = require("../table/utils") +const { DataSourceOperation, SortDirection, FieldTypes, RelationshipTypes } = require("../../../constants") +const { getAllExternalTables } = require("../table/utils") const { breakExternalTableId, generateRowIdField, @@ -35,17 +35,56 @@ function generateIdForRow(row, table) { return generateRowIdField(idParts) } -function outputProcessing(rows, table) { +function updateRelationshipColumns(rows, row, relationships, allTables) { + const columns = {} + for (let relationship of relationships) { + const linkedTable = allTables[relationship.tableName] + if (!linkedTable) { + continue + } + const display = linkedTable.primaryDisplay + const related = {} + if (display && row[display]) { + related.primaryDisplay = row[display] + } + related._id = row[relationship.to] + columns[relationship.from] = related + } + for (let [column, related] of Object.entries(columns)) { + if (!Array.isArray(rows[row._id][column])) { + rows[row._id][column] = [] + } + rows[row._id][column].push(related) + } + return rows +} + +function outputProcessing(rows, table, relationships, allTables) { // if no rows this is what is returned? Might be PG only if (rows[0].read === true) { return [] } + let finalRows = {} for (let row of rows) { row._id = generateIdForRow(row, table) - row.tableId = table._id - row._rev = "rev" + // this is a relationship of some sort + if (finalRows[row._id]) { + finalRows = updateRelationshipColumns(finalRows, row, relationships, allTables) + continue + } + const thisRow = {} + // filter the row down to what is actually the row (not joined) + for (let fieldName of Object.keys(table.schema)) { + thisRow[fieldName] = row[fieldName] + } + thisRow._id = row._id + thisRow.tableId = table._id + thisRow._rev = "rev" + finalRows[thisRow._id] = thisRow + // do this at end once its been added to the final rows + finalRows = updateRelationshipColumns(finalRows, row, relationships, allTables) } - return rows + return Object.values(finalRows) } function buildFilters(id, filters, table) { @@ -83,6 +122,26 @@ function buildFilters(id, filters, table) { } } +function buildRelationships(table) { + const relationships = [] + for (let [fieldName, field] of Object.entries(table.schema)) { + if (field.type !== FieldTypes.LINK) { + continue + } + // TODO: through field + if (field.relationshipType === RelationshipTypes.MANY_TO_MANY) { + continue + } + const broken = breakExternalTableId(field.tableId) + relationships.push({ + from: fieldName, + to: field.fieldName, + tableName: broken.tableName, + }) + } + return relationships +} + async function handleRequest( appId, operation, @@ -90,12 +149,14 @@ async function handleRequest( { id, row, filters, sort, paginate } = {} ) { let { datasourceId, tableName } = breakExternalTableId(tableId) - const table = await getExternalTable(appId, datasourceId, tableName) + const tables = await getAllExternalTables(appId, datasourceId) + const table = tables[tableName] if (!table) { throw `Unable to process query, table "${tableName}" not defined.` } // clean up row on ingress using schema filters = buildFilters(id, filters, table) + const relationships = buildRelationships(table) row = inputProcessing(row, table) if ( operation === DataSourceOperation.DELETE && @@ -116,6 +177,7 @@ async function handleRequest( filters, sort, paginate, + relationships, body: row, // pass an id filter into extra, purely for mysql/returning extra: { @@ -126,9 +188,9 @@ async function handleRequest( const response = await makeExternalQuery(appId, json) // we searched for rows in someway if (operation === DataSourceOperation.READ && Array.isArray(response)) { - return outputProcessing(response, table) + return outputProcessing(response, table, relationships, tables) } else { - row = outputProcessing(response, table)[0] + row = outputProcessing(response, table, relationships, tables)[0] return { row, table } } } @@ -270,7 +332,4 @@ exports.validate = async () => { return { valid: true } } -exports.fetchEnrichedRow = async () => { - // TODO: How does this work - throw "Not Implemented" -} +exports.fetchEnrichedRow = async () => {} diff --git a/packages/server/src/api/controllers/table/utils.js b/packages/server/src/api/controllers/table/utils.js index cdfd390027..78dae60ab1 100644 --- a/packages/server/src/api/controllers/table/utils.js +++ b/packages/server/src/api/controllers/table/utils.js @@ -204,15 +204,18 @@ class TableSaveFunctions { } } -exports.getExternalTable = async (appId, datasourceId, tableName) => { +exports.getAllExternalTables = async (appId, datasourceId) => { const db = new CouchDB(appId) const datasource = await db.get(datasourceId) if (!datasource || !datasource.entities) { throw "Datasource is not configured fully." } - return Object.values(datasource.entities).find( - entity => entity.name === tableName - ) + return datasource.entities +} + +exports.getExternalTable = async (appId, datasourceId, tableName) => { + const entities = await exports.getAllExternalTables(appId, datasourceId) + return entities[tableName] } exports.TableSaveFunctions = TableSaveFunctions diff --git a/packages/server/src/integrations/base/sql.js b/packages/server/src/integrations/base/sql.js index c7cc95fc3e..08eac09f0b 100644 --- a/packages/server/src/integrations/base/sql.js +++ b/packages/server/src/integrations/base/sql.js @@ -55,6 +55,25 @@ function addFilters(query, filters) { return query } +function addRelationships(query, fromTable, relationships) { + if (!relationships) { + return query + } + for (let relationship of relationships) { + const from = `${fromTable}.${relationship.from}` + const to = `${relationship.tableName}.${relationship.to}` + if (!relationship.through) { + query = query.innerJoin(relationship.tableName, from, to) + } else { + const through = relationship + query = query + .innerJoin(through.tableName, from, through.from) + .innerJoin(relationship.tableName, to, through.to) + } + } + return query +} + function buildCreate(knex, json, opts) { const { endpoint, body } = json let query = knex(endpoint.entityId) @@ -67,8 +86,9 @@ function buildCreate(knex, json, opts) { } function buildRead(knex, json, limit) { - let { endpoint, resource, filters, sort, paginate } = json - let query = knex(endpoint.entityId) + let { endpoint, resource, filters, sort, paginate, relationships } = json + const tableName = endpoint.entityId + let query = knex(tableName) // select all if not specified if (!resource) { resource = { fields: [] } @@ -81,6 +101,8 @@ function buildRead(knex, json, limit) { } // handle where query = addFilters(query, filters) + // handle join + query = addRelationships(query, tableName, relationships) // handle sorting if (sort) { for (let [key, value] of Object.entries(sort)) { diff --git a/packages/server/src/integrations/postgres.js b/packages/server/src/integrations/postgres.js index 6266e6ca64..f2d1f08162 100644 --- a/packages/server/src/integrations/postgres.js +++ b/packages/server/src/integrations/postgres.js @@ -153,6 +153,20 @@ class PostgresIntegration extends Sql { name: columnName, type: convertType(column.data_type, TYPE_MAP), } + + // // TODO: hack for testing + // if (tableName === "persons") { + // tables[tableName].primaryDisplay = "firstname" + // } + // if (columnName.toLowerCase() === "personid" && tableName === "tasks") { + // tables[tableName].schema[columnName] = { + // name: columnName, + // type: "link", + // tableId: buildExternalTableId(datasourceId, "persons"), + // relationshipType: "one-to-many", + // fieldName: "personid", + // } + // } } this.tables = tables }