diff --git a/packages/server/src/api/controllers/row/external.js b/packages/server/src/api/controllers/row/external.js index d259caf721..7786dbcd45 100644 --- a/packages/server/src/api/controllers/row/external.js +++ b/packages/server/src/api/controllers/row/external.js @@ -19,77 +19,103 @@ const { } = require("./externalUtils") const { processObjectSync } = require("@budibase/string-templates") -async function handleRequest( - appId, - operation, - tableId, - { id, row, filters, sort, paginate, tables } = {} -) { - let { datasourceId, tableName } = breakExternalTableId(tableId) - if (!tables) { - tables = await getAllExternalTables(appId, datasourceId) +class ExternalRequest { + constructor(appId, operation, tableId, tables) { + this.appId = appId + this.operation = operation + this.tableId = tableId + this.tables = tables } - 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, tables) - const processed = inputProcessing(row, table, tables) - row = processed.row - if ( - operation === DataSourceOperation.DELETE && - (filters == null || Object.keys(filters).length === 0) - ) { - throw "Deletion must be filtered" - } - let json = { - endpoint: { - datasourceId, - entityId: tableName, - operation, - }, - resource: { - // have to specify the fields to avoid column overlap - fields: buildFields(table, tables), - }, - filters, - sort, - paginate, - relationships, - body: row, - // pass an id filter into extra, purely for mysql/returning - extra: { - idFilter: buildFilters(id || generateIdForRow(row, table), {}, table), - }, - } - // can't really use response right now - const response = await makeExternalQuery(appId, json) - // handle many to many relationships now if we know the ID (could be auto increment) - if (processed.manyRelationships) { + + async handleManyRelationships(row, relationships) { + const { appId, tables } = this const promises = [] - for (let toInsert of processed.manyRelationships) { - const { tableName } = breakExternalTableId(toInsert.tableId) - delete toInsert.tableId + for (let relationship of relationships) { + const { tableId, isUpdate, id, ...rest } = relationship + const { datasourceId, tableName } = breakExternalTableId(tableId) + const linkedTable = tables[tableName] + if (!linkedTable) { + continue + } + const endpoint = { + datasourceId, + entityId: tableName, + operation: isUpdate ? DataSourceOperation.UPDATE : DataSourceOperation.CREATE, + } promises.push( makeExternalQuery(appId, { - endpoint: { - ...json.endpoint, - entityId: tableName, - }, + endpoint, // if we're doing many relationships then we're writing, only one response - body: processObjectSync(toInsert, response[0]), + body: processObjectSync(rest, row), + filters: buildFilters(id, {}, linkedTable) }) ) } await Promise.all(promises) } - const output = outputProcessing(response, table, relationships, tables) - // if reading it'll just be an array of rows, return whole thing - return operation === DataSourceOperation.READ && Array.isArray(response) - ? output - : { row: output[0], table } + + async run({ id, row, filters, sort, paginate }) { + const { appId, operation, tableId } = this + let { datasourceId, tableName } = breakExternalTableId(tableId) + if (!this.tables) { + this.tables = await getAllExternalTables(appId, datasourceId) + } + const table = this.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, this.tables) + const processed = inputProcessing(row, table, this.tables) + row = processed.row + if ( + operation === DataSourceOperation.DELETE && + (filters == null || Object.keys(filters).length === 0) + ) { + throw "Deletion must be filtered" + } + let json = { + endpoint: { + datasourceId, + entityId: tableName, + operation, + }, + resource: { + // have to specify the fields to avoid column overlap + fields: buildFields(table, this.tables), + }, + filters, + sort, + paginate, + relationships, + body: row, + // pass an id filter into extra, purely for mysql/returning + extra: { + idFilter: buildFilters(id || generateIdForRow(row, table), {}, table), + }, + } + // can't really use response right now + const response = await makeExternalQuery(appId, json) + // handle many to many relationships now if we know the ID (could be auto increment) + if (processed.manyRelationships) { + await this.handleManyRelationships(response[0], processed.manyRelationships) + } + const output = outputProcessing(response, table, relationships, this.tables) + // if reading it'll just be an array of rows, return whole thing + return operation === DataSourceOperation.READ && Array.isArray(response) + ? output + : { row: output[0], table } + } +} + +async function handleRequest( + appId, + operation, + tableId, + opts = {} +) { + return new ExternalRequest(appId, operation, tableId, opts.tables).run(opts) } exports.patch = async ctx => { diff --git a/packages/server/src/api/controllers/row/externalUtils.js b/packages/server/src/api/controllers/row/externalUtils.js index 226a8761c8..71f80ddb5a 100644 --- a/packages/server/src/api/controllers/row/externalUtils.js +++ b/packages/server/src/api/controllers/row/externalUtils.js @@ -18,6 +18,10 @@ function basicProcessing(row, table) { return thisRow } +function isMany(field) { + return field.relationshipType.split("-")[0] === "many" +} + exports.inputProcessing = (row, table, allTables) => { if (!row) { return { row, manyRelationships: [] } @@ -40,19 +44,24 @@ exports.inputProcessing = (row, table, allTables) => { continue } const linkTable = allTables[linkTableName] - if (!field.through) { + if (!isMany(field)) { // we don't really support composite keys for relationships, this is why [0] is used newRow[field.foreignKey || linkTable.primary] = breakRowIdField( row[key][0] )[0] } else { + // we're not inserting a doc, will be a bunch of update calls + const isUpdate = !field.through + const thisKey = isUpdate ? "id" : linkTable.primary + const otherKey = isUpdate ? field.foreignKey : table.primary row[key].map(relationship => { // we don't really support composite keys for relationships, this is why [0] is used manyRelationships.push({ - tableId: field.through, - [linkTable.primary]: breakRowIdField(relationship)[0], + tableId: field.through || field.tableId, + isUpdate, + [thisKey]: breakRowIdField(relationship)[0], // leave the ID for enrichment later - [table.primary]: `{{ ${table.primary} }}`, + [otherKey]: `{{ ${table.primary} }}`, }) }) } @@ -65,13 +74,18 @@ exports.inputProcessing = (row, table, allTables) => { exports.generateIdForRow = (row, table) => { if (!row) { - return + return null } const primary = table.primary // build id array let idParts = [] for (let field of primary) { - idParts.push(row[field]) + if (row[field]) { + idParts.push(row[field]) + } + } + if (idParts.length === 0) { + return null } return generateRowIdField(idParts) } @@ -84,6 +98,9 @@ exports.updateRelationshipColumns = (row, rows, relationships, allTables) => { continue } let linked = basicProcessing(row, linkedTable) + if (!linked._id) { + continue + } // if not returning full docs then get the minimal links out const display = linkedTable.primaryDisplay linked = { @@ -193,7 +210,7 @@ exports.buildFilters = (id, filters, table) => { return filters } // if used as URL parameter it will have been joined - if (typeof idCopy === "string") { + if (!Array.isArray(idCopy)) { idCopy = breakRowIdField(idCopy) } const equal = {} diff --git a/packages/server/src/integrations/postgres.ts b/packages/server/src/integrations/postgres.ts index e867630744..31bd24440f 100644 --- a/packages/server/src/integrations/postgres.ts +++ b/packages/server/src/integrations/postgres.ts @@ -174,54 +174,6 @@ module PostgresModule { name: columnName, type, } - - // TODO: hack for testing - // if (tableName === "persons") { - // tables[tableName].primaryDisplay = "firstname" - // } - // if (tableName === "products") { - // tables[tableName].primaryDisplay = "productname" - // } - // if (tableName === "tasks") { - // tables[tableName].primaryDisplay = "taskname" - // } - // if (tableName === "products") { - // tables[tableName].schema["tasks"] = { - // name: "tasks", - // type: "link", - // tableId: buildExternalTableId(datasourceId, "tasks"), - // relationshipType: "many-to-many", - // through: buildExternalTableId(datasourceId, "products_tasks"), - // fieldName: "taskid", - // } - // } - // if (tableName === "persons") { - // tables[tableName].schema["tasks"] = { - // name: "tasks", - // type: "link", - // tableId: buildExternalTableId(datasourceId, "tasks"), - // relationshipType: "many-to-one", - // fieldName: "personid", - // } - // } - // if (tableName === "tasks") { - // tables[tableName].schema["products"] = { - // name: "products", - // type: "link", - // tableId: buildExternalTableId(datasourceId, "products"), - // relationshipType: "many-to-many", - // through: buildExternalTableId(datasourceId, "products_tasks"), - // fieldName: "productid", - // } - // tables[tableName].schema["people"] = { - // name: "people", - // type: "link", - // tableId: buildExternalTableId(datasourceId, "persons"), - // relationshipType: "one-to-many", - // fieldName: "personid", - // foreignKey: "personid", - // } - // } } this.tables = tables } diff --git a/packages/server/src/integrations/utils.ts b/packages/server/src/integrations/utils.ts index 581efdc605..b668e6edeb 100644 --- a/packages/server/src/integrations/utils.ts +++ b/packages/server/src/integrations/utils.ts @@ -33,7 +33,9 @@ export function breakRowIdField(_id: string) { if (!_id) { return null } - return JSON.parse(decodeURIComponent(_id)) + const decoded = decodeURIComponent(_id) + const parsed = JSON.parse(decoded) + return Array.isArray(parsed) ? parsed : [parsed] } export function convertType(type: string, map: { [key: string]: any }) {