mirror of https://github.com/Budibase/budibase.git
20 changed files with 583 additions and 190 deletions
@ -0,0 +1,44 @@ |
|||
<script> |
|||
import { ModalContent, Body, Input } from "@budibase/bbui" |
|||
import { tables, datasources } from "stores/backend" |
|||
import { goto } from "@roxi/routify" |
|||
|
|||
export let datasource |
|||
|
|||
let name = "" |
|||
|
|||
$: valid = name && name.length > 0 |
|||
|
|||
function buildDefaultTable(tableName, datasourceId) { |
|||
return { |
|||
name: tableName, |
|||
type: "external", |
|||
primary: ["id"], |
|||
sourceId: datasourceId, |
|||
schema: { |
|||
id: { |
|||
autocolumn: true, |
|||
type: "number", |
|||
}, |
|||
}, |
|||
} |
|||
} |
|||
|
|||
async function saveTable() { |
|||
const table = await tables.save(buildDefaultTable(name, datasource._id)) |
|||
await datasources.fetch() |
|||
$goto(`../../table/${table._id}`) |
|||
} |
|||
</script> |
|||
|
|||
<ModalContent |
|||
title="Create new table" |
|||
confirmText="Create" |
|||
onConfirm={saveTable} |
|||
disabled={!valid} |
|||
> |
|||
<Body |
|||
>Provide a name for your new table; you can add columns once it is created.</Body |
|||
> |
|||
<Input label="Table Name" bind:value={name} /> |
|||
</ModalContent> |
|||
@ -0,0 +1,116 @@ |
|||
const CouchDB = require("../../../db") |
|||
const { |
|||
buildExternalTableId, |
|||
breakExternalTableId, |
|||
} = require("../../../integrations/utils") |
|||
const { getTable } = require("./utils") |
|||
const { DataSourceOperation, FieldTypes } = require("../../../constants") |
|||
const { makeExternalQuery } = require("../../../integrations/base/utils") |
|||
const { cloneDeep } = require("lodash/fp") |
|||
|
|||
async function makeTableRequest( |
|||
datasource, |
|||
operation, |
|||
table, |
|||
tables, |
|||
oldTable = null |
|||
) { |
|||
const json = { |
|||
endpoint: { |
|||
datasourceId: datasource._id, |
|||
entityId: table._id, |
|||
operation, |
|||
}, |
|||
meta: { |
|||
tables, |
|||
}, |
|||
table, |
|||
} |
|||
if (oldTable) { |
|||
json.meta.table = oldTable |
|||
} |
|||
return makeExternalQuery(datasource, json) |
|||
} |
|||
|
|||
function getDatasourceId(table) { |
|||
if (!table) { |
|||
throw "No table supplied" |
|||
} |
|||
if (table.sourceId) { |
|||
return table.sourceId |
|||
} |
|||
return breakExternalTableId(table._id).datasourceId |
|||
} |
|||
|
|||
exports.save = async function (ctx) { |
|||
const appId = ctx.appId |
|||
const table = ctx.request.body |
|||
// can't do this
|
|||
delete table.dataImport |
|||
const datasourceId = getDatasourceId(ctx.request.body) |
|||
let tableToSave = { |
|||
type: "table", |
|||
_id: buildExternalTableId(datasourceId, table.name), |
|||
...table, |
|||
} |
|||
|
|||
let oldTable |
|||
if (ctx.request.body && ctx.request.body._id) { |
|||
oldTable = await getTable(appId, ctx.request.body._id) |
|||
} |
|||
|
|||
const db = new CouchDB(appId) |
|||
const datasource = await db.get(datasourceId) |
|||
const tables = datasource.entities |
|||
|
|||
// check if relations need setup
|
|||
for (let [key, schema] of Object.entries(tableToSave.schema)) { |
|||
// TODO: this assumes all relationships are the same, need to handle cardinality and many to many
|
|||
if (schema.type === FieldTypes.LINK) { |
|||
const relatedTable = Object.values(tables).find( |
|||
table => table._id === schema.tableId |
|||
) |
|||
const relatedField = schema.fieldName |
|||
const foreignKey = `fk_${relatedTable.name}_${schema.fieldName}` |
|||
// create foreign key
|
|||
tableToSave.schema[foreignKey] = { type: FieldTypes.NUMBER } |
|||
// setup the relation in other table and this one
|
|||
schema.foreignKey = foreignKey |
|||
schema.fieldName = foreignKey |
|||
schema.main = true |
|||
const relatedSchema = cloneDeep(schema) |
|||
relatedSchema.fieldName = key |
|||
delete relatedSchema.main |
|||
relatedTable.schema[relatedField] = relatedSchema |
|||
} |
|||
} |
|||
|
|||
const operation = oldTable |
|||
? DataSourceOperation.UPDATE_TABLE |
|||
: DataSourceOperation.CREATE_TABLE |
|||
await makeTableRequest(datasource, operation, tableToSave, tables, oldTable) |
|||
|
|||
// store it into couch now for budibase reference
|
|||
datasource.entities[tableToSave.name] = tableToSave |
|||
await db.put(datasource) |
|||
|
|||
return tableToSave |
|||
} |
|||
|
|||
exports.destroy = async function (ctx) { |
|||
const appId = ctx.appId |
|||
const tableToDelete = await getTable(appId, ctx.params.tableId) |
|||
const datasourceId = getDatasourceId(tableToDelete) |
|||
|
|||
const db = new CouchDB(appId) |
|||
const datasource = await db.get(datasourceId) |
|||
const tables = datasource.entities |
|||
|
|||
const operation = DataSourceOperation.DELETE_TABLE |
|||
await makeTableRequest(datasource, operation, tableToDelete, tables) |
|||
|
|||
delete datasource.entities[tableToDelete.name] |
|||
await db.put(datasource) |
|||
|
|||
return tableToDelete |
|||
} |
|||
@ -0,0 +1,138 @@ |
|||
const CouchDB = require("../../../db") |
|||
const linkRows = require("../../../db/linkedRows") |
|||
const { getRowParams, generateTableID } = require("../../../db/utils") |
|||
const { FieldTypes } = require("../../../constants") |
|||
const { TableSaveFunctions } = require("./utils") |
|||
|
|||
exports.save = async function (ctx) { |
|||
const appId = ctx.appId |
|||
const db = new CouchDB(appId) |
|||
const { dataImport, ...rest } = ctx.request.body |
|||
let tableToSave = { |
|||
type: "table", |
|||
_id: generateTableID(), |
|||
views: {}, |
|||
...rest, |
|||
} |
|||
|
|||
// if the table obj had an _id then it will have been retrieved
|
|||
let oldTable |
|||
if (ctx.request.body && ctx.request.body._id) { |
|||
oldTable = await db.get(ctx.request.body._id) |
|||
} |
|||
|
|||
// saving a table is a complex operation, involving many different steps, this
|
|||
// has been broken out into a utility to make it more obvious/easier to manipulate
|
|||
const tableSaveFunctions = new TableSaveFunctions({ |
|||
db, |
|||
ctx, |
|||
oldTable, |
|||
dataImport, |
|||
}) |
|||
tableToSave = await tableSaveFunctions.before(tableToSave) |
|||
|
|||
// make sure that types don't change of a column, have to remove
|
|||
// the column if you want to change the type
|
|||
if (oldTable && oldTable.schema) { |
|||
for (let propKey of Object.keys(tableToSave.schema)) { |
|||
let column = tableToSave.schema[propKey] |
|||
let oldColumn = oldTable.schema[propKey] |
|||
if (oldColumn && oldColumn.type === "internal") { |
|||
oldColumn.type = "auto" |
|||
} |
|||
if (oldColumn && oldColumn.type !== column.type) { |
|||
ctx.throw(400, "Cannot change the type of a column") |
|||
} |
|||
} |
|||
} |
|||
|
|||
// Don't rename if the name is the same
|
|||
let { _rename } = tableToSave |
|||
/* istanbul ignore next */ |
|||
if (_rename && _rename.old === _rename.updated) { |
|||
_rename = null |
|||
delete tableToSave._rename |
|||
} |
|||
|
|||
// rename row fields when table column is renamed
|
|||
/* istanbul ignore next */ |
|||
if (_rename && tableToSave.schema[_rename.updated].type === FieldTypes.LINK) { |
|||
ctx.throw(400, "Cannot rename a linked column.") |
|||
} |
|||
|
|||
tableToSave = await tableSaveFunctions.mid(tableToSave) |
|||
|
|||
// update schema of non-statistics views when new columns are added
|
|||
for (let view in tableToSave.views) { |
|||
const tableView = tableToSave.views[view] |
|||
if (!tableView) continue |
|||
|
|||
if (tableView.schema.group || tableView.schema.field) continue |
|||
tableView.schema = tableToSave.schema |
|||
} |
|||
|
|||
// update linked rows
|
|||
try { |
|||
const linkResp = await linkRows.updateLinks({ |
|||
appId, |
|||
eventType: oldTable |
|||
? linkRows.EventType.TABLE_UPDATED |
|||
: linkRows.EventType.TABLE_SAVE, |
|||
table: tableToSave, |
|||
oldTable: oldTable, |
|||
}) |
|||
if (linkResp != null && linkResp._rev) { |
|||
tableToSave._rev = linkResp._rev |
|||
} |
|||
} catch (err) { |
|||
ctx.throw(400, err) |
|||
} |
|||
|
|||
// don't perform any updates until relationships have been
|
|||
// checked by the updateLinks function
|
|||
const updatedRows = tableSaveFunctions.getUpdatedRows() |
|||
if (updatedRows && updatedRows.length !== 0) { |
|||
await db.bulkDocs(updatedRows) |
|||
} |
|||
const result = await db.put(tableToSave) |
|||
tableToSave._rev = result.rev |
|||
|
|||
tableToSave = await tableSaveFunctions.after(tableToSave) |
|||
|
|||
return tableToSave |
|||
} |
|||
|
|||
exports.destroy = async function (ctx) { |
|||
const appId = ctx.appId |
|||
const db = new CouchDB(appId) |
|||
const tableToDelete = await db.get(ctx.params.tableId) |
|||
|
|||
// Delete all rows for that table
|
|||
const rows = await db.allDocs( |
|||
getRowParams(ctx.params.tableId, null, { |
|||
include_docs: true, |
|||
}) |
|||
) |
|||
await db.bulkDocs(rows.rows.map(row => ({ ...row.doc, _deleted: true }))) |
|||
|
|||
// update linked rows
|
|||
await linkRows.updateLinks({ |
|||
appId, |
|||
eventType: linkRows.EventType.TABLE_DELETE, |
|||
table: tableToDelete, |
|||
}) |
|||
|
|||
// don't remove the table itself until very end
|
|||
await db.remove(tableToDelete) |
|||
|
|||
// remove table search index
|
|||
const currentIndexes = await db.getIndexes() |
|||
const existingIndex = currentIndexes.indexes.find( |
|||
existing => existing.name === `search:${ctx.params.tableId}` |
|||
) |
|||
if (existingIndex) { |
|||
await db.deleteIndex(existingIndex) |
|||
} |
|||
|
|||
return tableToDelete |
|||
} |
|||
@ -0,0 +1,132 @@ |
|||
import { Knex, knex } from "knex" |
|||
import { Table } from "../../definitions/common" |
|||
import { Operation, QueryJson } from "../../definitions/datasource" |
|||
import { breakExternalTableId } from "../utils" |
|||
import SchemaBuilder = Knex.SchemaBuilder |
|||
import CreateTableBuilder = Knex.CreateTableBuilder |
|||
const { FieldTypes } = require("../../constants") |
|||
|
|||
function generateSchema(schema: CreateTableBuilder, table: Table, tables: Record<string, Table>, oldTable: null | Table = null) { |
|||
let primaryKey = table && table.primary ? table.primary[0] : null |
|||
// can't change primary once its set for now
|
|||
if (primaryKey && !oldTable) { |
|||
schema.increments(primaryKey).primary() |
|||
} |
|||
const foreignKeys = Object.values(table.schema).map(col => col.foreignKey) |
|||
for (let [key, column] of Object.entries(table.schema)) { |
|||
// skip things that are already correct
|
|||
const oldColumn = oldTable ? oldTable.schema[key] : null |
|||
if ((oldColumn && oldColumn.type === column.type) || primaryKey === key) { |
|||
continue |
|||
} |
|||
switch (column.type) { |
|||
case FieldTypes.STRING: case FieldTypes.OPTIONS: case FieldTypes.LONGFORM: |
|||
schema.string(key) |
|||
break |
|||
case FieldTypes.NUMBER: |
|||
if (foreignKeys.indexOf(key) === -1) { |
|||
schema.float(key) |
|||
} |
|||
break |
|||
case FieldTypes.BOOLEAN: |
|||
schema.boolean(key) |
|||
break |
|||
case FieldTypes.DATETIME: |
|||
schema.datetime(key) |
|||
break |
|||
case FieldTypes.ARRAY: |
|||
schema.json(key) |
|||
break |
|||
case FieldTypes.LINK: |
|||
if (!column.foreignKey || !column.tableId) { |
|||
throw "Invalid relationship schema" |
|||
} |
|||
const { tableName } = breakExternalTableId(column.tableId) |
|||
// @ts-ignore
|
|||
const relatedTable = tables[tableName] |
|||
if (!relatedTable) { |
|||
throw "Referenced table doesn't exist" |
|||
} |
|||
schema.integer(column.foreignKey).unsigned() |
|||
schema.foreign(column.foreignKey).references(`${tableName}.${relatedTable.primary[0]}`) |
|||
} |
|||
} |
|||
return schema |
|||
} |
|||
|
|||
function buildCreateTable( |
|||
knex: Knex, |
|||
table: Table, |
|||
tables: Record<string, Table>, |
|||
): SchemaBuilder { |
|||
return knex.schema.createTable(table.name, schema => { |
|||
generateSchema(schema, table, tables) |
|||
}) |
|||
} |
|||
|
|||
function buildUpdateTable( |
|||
knex: Knex, |
|||
table: Table, |
|||
tables: Record<string, Table>, |
|||
oldTable: Table, |
|||
): SchemaBuilder { |
|||
return knex.schema.alterTable(table.name, schema => { |
|||
generateSchema(schema, table, tables, oldTable) |
|||
}) |
|||
} |
|||
|
|||
function buildDeleteTable( |
|||
knex: Knex, |
|||
table: Table, |
|||
): SchemaBuilder { |
|||
return knex.schema.dropTable(table.name) |
|||
} |
|||
|
|||
class SqlTableQueryBuilder { |
|||
private readonly sqlClient: string |
|||
|
|||
// pass through client to get flavour of SQL
|
|||
constructor(client: string) { |
|||
this.sqlClient = client |
|||
} |
|||
|
|||
getSqlClient(): string { |
|||
return this.sqlClient |
|||
} |
|||
|
|||
/** |
|||
* @param json the input JSON structure from which an SQL query will be built. |
|||
* @return {string} the operation that was found in the JSON. |
|||
*/ |
|||
_operation(json: QueryJson): Operation { |
|||
return json.endpoint.operation |
|||
} |
|||
|
|||
_tableQuery(json: QueryJson): any { |
|||
const client = knex({ client: this.sqlClient }) |
|||
let query |
|||
if (!json.table || !json.meta || !json.meta.tables) { |
|||
throw "Cannot execute without table being specified" |
|||
} |
|||
switch (this._operation(json)) { |
|||
case Operation.CREATE_TABLE: |
|||
query = buildCreateTable(client, json.table, json.meta.tables) |
|||
break |
|||
case Operation.UPDATE_TABLE: |
|||
if (!json.meta || !json.meta.table) { |
|||
throw "Must specify old table for update" |
|||
} |
|||
query = buildUpdateTable(client, json.table, json.meta.tables, json.meta.table) |
|||
break |
|||
case Operation.DELETE_TABLE: |
|||
query = buildDeleteTable(client, json.table) |
|||
break |
|||
default: |
|||
throw "Table operation is of unknown type" |
|||
} |
|||
return query.toSQL() |
|||
} |
|||
} |
|||
|
|||
export default SqlTableQueryBuilder |
|||
module.exports = SqlTableQueryBuilder |
|||
@ -0,0 +1,19 @@ |
|||
import { QueryJson } from "../../definitions/datasource" |
|||
import { Datasource } from "../../definitions/common" |
|||
|
|||
module DatasourceUtils { |
|||
const { integrations } = require("../index") |
|||
|
|||
export async function makeExternalQuery(datasource: Datasource, json: QueryJson) { |
|||
const Integration = integrations[datasource.source] |
|||
// query is the opinionated function
|
|||
if (Integration.prototype.query) { |
|||
const integration = new Integration(datasource.config) |
|||
return integration.query(json) |
|||
} else { |
|||
throw "Datasource does not support query." |
|||
} |
|||
} |
|||
|
|||
module.exports.makeExternalQuery = makeExternalQuery |
|||
} |
|||
Loading…
Reference in new issue