Merge branch 'feature/sql-relationships' of github.com:Budibase/budibase into feature/opinionated-relationships-ui
This commit is contained in:
commit
a4f75cde17
5 changed files with 177 additions and 29 deletions
|
@ -1,6 +1,11 @@
|
||||||
const { makeExternalQuery } = require("./utils")
|
const { makeExternalQuery } = require("./utils")
|
||||||
const { DataSourceOperation, SortDirection } = require("../../../constants")
|
const {
|
||||||
const { getExternalTable } = require("../table/utils")
|
DataSourceOperation,
|
||||||
|
SortDirection,
|
||||||
|
FieldTypes,
|
||||||
|
RelationshipTypes,
|
||||||
|
} = require("../../../constants")
|
||||||
|
const { getAllExternalTables } = require("../table/utils")
|
||||||
const {
|
const {
|
||||||
breakExternalTableId,
|
breakExternalTableId,
|
||||||
generateRowIdField,
|
generateRowIdField,
|
||||||
|
@ -35,17 +40,66 @@ function generateIdForRow(row, table) {
|
||||||
return generateRowIdField(idParts)
|
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 no rows this is what is returned? Might be PG only
|
||||||
if (rows[0].read === true) {
|
if (rows[0].read === true) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
let finalRows = {}
|
||||||
for (let row of rows) {
|
for (let row of rows) {
|
||||||
row._id = generateIdForRow(row, table)
|
row._id = generateIdForRow(row, table)
|
||||||
row.tableId = table._id
|
// this is a relationship of some sort
|
||||||
row._rev = "rev"
|
if (finalRows[row._id]) {
|
||||||
|
finalRows = updateRelationshipColumns(
|
||||||
|
finalRows,
|
||||||
|
row,
|
||||||
|
relationships,
|
||||||
|
allTables
|
||||||
|
)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
return rows
|
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 Object.values(finalRows)
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildFilters(id, filters, table) {
|
function buildFilters(id, filters, table) {
|
||||||
|
@ -83,6 +137,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(
|
async function handleRequest(
|
||||||
appId,
|
appId,
|
||||||
operation,
|
operation,
|
||||||
|
@ -90,12 +164,14 @@ async function handleRequest(
|
||||||
{ id, row, filters, sort, paginate } = {}
|
{ id, row, filters, sort, paginate } = {}
|
||||||
) {
|
) {
|
||||||
let { datasourceId, tableName } = breakExternalTableId(tableId)
|
let { datasourceId, tableName } = breakExternalTableId(tableId)
|
||||||
const table = await getExternalTable(appId, datasourceId, tableName)
|
const tables = await getAllExternalTables(appId, datasourceId)
|
||||||
|
const table = tables[tableName]
|
||||||
if (!table) {
|
if (!table) {
|
||||||
throw `Unable to process query, table "${tableName}" not defined.`
|
throw `Unable to process query, table "${tableName}" not defined.`
|
||||||
}
|
}
|
||||||
// clean up row on ingress using schema
|
// clean up row on ingress using schema
|
||||||
filters = buildFilters(id, filters, table)
|
filters = buildFilters(id, filters, table)
|
||||||
|
const relationships = buildRelationships(table)
|
||||||
row = inputProcessing(row, table)
|
row = inputProcessing(row, table)
|
||||||
if (
|
if (
|
||||||
operation === DataSourceOperation.DELETE &&
|
operation === DataSourceOperation.DELETE &&
|
||||||
|
@ -116,6 +192,7 @@ async function handleRequest(
|
||||||
filters,
|
filters,
|
||||||
sort,
|
sort,
|
||||||
paginate,
|
paginate,
|
||||||
|
relationships,
|
||||||
body: row,
|
body: row,
|
||||||
// pass an id filter into extra, purely for mysql/returning
|
// pass an id filter into extra, purely for mysql/returning
|
||||||
extra: {
|
extra: {
|
||||||
|
@ -126,9 +203,9 @@ async function handleRequest(
|
||||||
const response = await makeExternalQuery(appId, json)
|
const response = await makeExternalQuery(appId, json)
|
||||||
// we searched for rows in someway
|
// we searched for rows in someway
|
||||||
if (operation === DataSourceOperation.READ && Array.isArray(response)) {
|
if (operation === DataSourceOperation.READ && Array.isArray(response)) {
|
||||||
return outputProcessing(response, table)
|
return outputProcessing(response, table, relationships, tables)
|
||||||
} else {
|
} else {
|
||||||
row = outputProcessing(response, table)[0]
|
row = outputProcessing(response, table, relationships, tables)[0]
|
||||||
return { row, table }
|
return { row, table }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -270,7 +347,4 @@ exports.validate = async () => {
|
||||||
return { valid: true }
|
return { valid: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.fetchEnrichedRow = async () => {
|
exports.fetchEnrichedRow = async () => {}
|
||||||
// TODO: How does this work
|
|
||||||
throw "Not Implemented"
|
|
||||||
}
|
|
||||||
|
|
|
@ -204,15 +204,18 @@ class TableSaveFunctions {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.getExternalTable = async (appId, datasourceId, tableName) => {
|
exports.getAllExternalTables = async (appId, datasourceId) => {
|
||||||
const db = new CouchDB(appId)
|
const db = new CouchDB(appId)
|
||||||
const datasource = await db.get(datasourceId)
|
const datasource = await db.get(datasourceId)
|
||||||
if (!datasource || !datasource.entities) {
|
if (!datasource || !datasource.entities) {
|
||||||
throw "Datasource is not configured fully."
|
throw "Datasource is not configured fully."
|
||||||
}
|
}
|
||||||
return Object.values(datasource.entities).find(
|
return datasource.entities
|
||||||
entity => entity.name === tableName
|
}
|
||||||
)
|
|
||||||
|
exports.getExternalTable = async (appId, datasourceId, tableName) => {
|
||||||
|
const entities = await exports.getAllExternalTables(appId, datasourceId)
|
||||||
|
return entities[tableName]
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.TableSaveFunctions = TableSaveFunctions
|
exports.TableSaveFunctions = TableSaveFunctions
|
||||||
|
|
|
@ -74,6 +74,17 @@ export interface SearchFilters {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RelationshipsJson {
|
||||||
|
through?: {
|
||||||
|
from: string
|
||||||
|
to: string
|
||||||
|
tableName: string
|
||||||
|
}
|
||||||
|
from: string
|
||||||
|
to: string
|
||||||
|
tableName: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface QueryJson {
|
export interface QueryJson {
|
||||||
endpoint: {
|
endpoint: {
|
||||||
datasourceId: string
|
datasourceId: string
|
||||||
|
@ -92,9 +103,10 @@ export interface QueryJson {
|
||||||
page: string | number
|
page: string | number
|
||||||
}
|
}
|
||||||
body?: object
|
body?: object
|
||||||
extra: {
|
extra?: {
|
||||||
idFilter?: SearchFilters
|
idFilter?: SearchFilters
|
||||||
}
|
}
|
||||||
|
relationships?: RelationshipsJson[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SqlQuery {
|
export interface SqlQuery {
|
||||||
|
|
|
@ -6,12 +6,15 @@ import {
|
||||||
QueryOptions,
|
QueryOptions,
|
||||||
SortDirection,
|
SortDirection,
|
||||||
Operation,
|
Operation,
|
||||||
|
RelationshipsJson,
|
||||||
} from "./definitions"
|
} from "./definitions"
|
||||||
|
|
||||||
|
type KnexQuery = Knex.QueryBuilder | Knex
|
||||||
|
|
||||||
function addFilters(
|
function addFilters(
|
||||||
query: any,
|
query: KnexQuery,
|
||||||
filters: SearchFilters | undefined
|
filters: SearchFilters | undefined
|
||||||
): Knex.QueryBuilder {
|
): KnexQuery {
|
||||||
function iterate(
|
function iterate(
|
||||||
structure: { [key: string]: any },
|
structure: { [key: string]: any },
|
||||||
fn: (key: string, value: any) => void
|
fn: (key: string, value: any) => void
|
||||||
|
@ -67,9 +70,38 @@ function addFilters(
|
||||||
return query
|
return query
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildCreate(knex: Knex, json: QueryJson, opts: QueryOptions) {
|
function addRelationships(
|
||||||
|
query: KnexQuery,
|
||||||
|
fromTable: string,
|
||||||
|
relationships: RelationshipsJson[] | undefined
|
||||||
|
): KnexQuery {
|
||||||
|
if (!relationships) {
|
||||||
|
return query
|
||||||
|
}
|
||||||
|
for (let relationship of relationships) {
|
||||||
|
const from = `${fromTable}.${relationship.from}`
|
||||||
|
const to = `${relationship.tableName}.${relationship.to}`
|
||||||
|
if (!relationship.through) {
|
||||||
|
// @ts-ignore
|
||||||
|
query = query.innerJoin(relationship.tableName, from, to)
|
||||||
|
} else {
|
||||||
|
const through = relationship
|
||||||
|
query = query
|
||||||
|
// @ts-ignore
|
||||||
|
.innerJoin(through.tableName, from, through.from)
|
||||||
|
.innerJoin(relationship.tableName, to, through.to)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return query
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCreate(
|
||||||
|
knex: Knex,
|
||||||
|
json: QueryJson,
|
||||||
|
opts: QueryOptions
|
||||||
|
): KnexQuery {
|
||||||
const { endpoint, body } = json
|
const { endpoint, body } = json
|
||||||
let query = knex(endpoint.entityId)
|
let query: KnexQuery = knex(endpoint.entityId)
|
||||||
// mysql can't use returning
|
// mysql can't use returning
|
||||||
if (opts.disableReturning) {
|
if (opts.disableReturning) {
|
||||||
return query.insert(body)
|
return query.insert(body)
|
||||||
|
@ -78,9 +110,10 @@ function buildCreate(knex: Knex, json: QueryJson, opts: QueryOptions) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildRead(knex: Knex, json: QueryJson, limit: number) {
|
function buildRead(knex: Knex, json: QueryJson, limit: number): KnexQuery {
|
||||||
let { endpoint, resource, filters, sort, paginate } = json
|
let { endpoint, resource, filters, sort, paginate, relationships } = json
|
||||||
let query: Knex.QueryBuilder = knex(endpoint.entityId)
|
const tableName = endpoint.entityId
|
||||||
|
let query: KnexQuery = knex(tableName)
|
||||||
// select all if not specified
|
// select all if not specified
|
||||||
if (!resource) {
|
if (!resource) {
|
||||||
resource = { fields: [] }
|
resource = { fields: [] }
|
||||||
|
@ -93,6 +126,8 @@ function buildRead(knex: Knex, json: QueryJson, limit: number) {
|
||||||
}
|
}
|
||||||
// handle where
|
// handle where
|
||||||
query = addFilters(query, filters)
|
query = addFilters(query, filters)
|
||||||
|
// handle join
|
||||||
|
query = addRelationships(query, tableName, relationships)
|
||||||
// handle sorting
|
// handle sorting
|
||||||
if (sort) {
|
if (sort) {
|
||||||
for (let [key, value] of Object.entries(sort)) {
|
for (let [key, value] of Object.entries(sort)) {
|
||||||
|
@ -114,9 +149,13 @@ function buildRead(knex: Knex, json: QueryJson, limit: number) {
|
||||||
return query
|
return query
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildUpdate(knex: Knex, json: QueryJson, opts: QueryOptions) {
|
function buildUpdate(
|
||||||
|
knex: Knex,
|
||||||
|
json: QueryJson,
|
||||||
|
opts: QueryOptions
|
||||||
|
): KnexQuery {
|
||||||
const { endpoint, body, filters } = json
|
const { endpoint, body, filters } = json
|
||||||
let query = knex(endpoint.entityId)
|
let query: KnexQuery = knex(endpoint.entityId)
|
||||||
query = addFilters(query, filters)
|
query = addFilters(query, filters)
|
||||||
// mysql can't use returning
|
// mysql can't use returning
|
||||||
if (opts.disableReturning) {
|
if (opts.disableReturning) {
|
||||||
|
@ -126,9 +165,13 @@ function buildUpdate(knex: Knex, json: QueryJson, opts: QueryOptions) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildDelete(knex: Knex, json: QueryJson, opts: QueryOptions) {
|
function buildDelete(
|
||||||
|
knex: Knex,
|
||||||
|
json: QueryJson,
|
||||||
|
opts: QueryOptions
|
||||||
|
): KnexQuery {
|
||||||
const { endpoint, filters } = json
|
const { endpoint, filters } = json
|
||||||
let query = knex(endpoint.entityId)
|
let query: KnexQuery = knex(endpoint.entityId)
|
||||||
query = addFilters(query, filters)
|
query = addFilters(query, filters)
|
||||||
// mysql can't use returning
|
// mysql can't use returning
|
||||||
if (opts.disableReturning) {
|
if (opts.disableReturning) {
|
||||||
|
@ -180,6 +223,8 @@ class SqlQueryBuilder {
|
||||||
default:
|
default:
|
||||||
throw `Operation type is not supported by SQL query builder`
|
throw `Operation type is not supported by SQL query builder`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
return query.toSQL().toNative()
|
return query.toSQL().toNative()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -174,6 +174,20 @@ module PostgresModule {
|
||||||
name: columnName,
|
name: columnName,
|
||||||
type,
|
type,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// // 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
|
this.tables = tables
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue