mirror of https://github.com/Budibase/budibase.git
committed by
GitHub
27 changed files with 949 additions and 255 deletions
@ -0,0 +1,54 @@ |
|||||
|
<script> |
||||
|
import { ActionButton, Modal, notifications } from "@budibase/bbui" |
||||
|
import CreateEditRelationship from "../../Datasources/CreateEditRelationship.svelte" |
||||
|
import { datasources, tables } from "../../../../stores/backend" |
||||
|
import { createEventDispatcher } from "svelte" |
||||
|
|
||||
|
export let table |
||||
|
const dispatch = createEventDispatcher() |
||||
|
|
||||
|
$: plusTables = datasource?.plus |
||||
|
? Object.values(datasource?.entities || {}) |
||||
|
: [] |
||||
|
$: datasource = $datasources.list.find( |
||||
|
source => source._id === table?.sourceId |
||||
|
) |
||||
|
|
||||
|
let modal |
||||
|
|
||||
|
async function saveRelationship() { |
||||
|
try { |
||||
|
// Create datasource |
||||
|
await datasources.save(datasource) |
||||
|
notifications.success(`Relationship information saved.`) |
||||
|
const tableList = await tables.fetch() |
||||
|
await tables.select(tableList.find(tbl => tbl._id === table._id)) |
||||
|
dispatch("updatecolumns") |
||||
|
} catch (err) { |
||||
|
notifications.error(`Error saving relationship info: ${err}`) |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
{#if table.sourceId} |
||||
|
<div> |
||||
|
<ActionButton |
||||
|
icon="DataCorrelated" |
||||
|
primary |
||||
|
size="S" |
||||
|
quiet |
||||
|
on:click={modal.show} |
||||
|
> |
||||
|
Define existing relationship |
||||
|
</ActionButton> |
||||
|
</div> |
||||
|
<Modal bind:this={modal}> |
||||
|
<CreateEditRelationship |
||||
|
{datasource} |
||||
|
save={saveRelationship} |
||||
|
close={modal.hide} |
||||
|
{plusTables} |
||||
|
selectedFromTable={table} |
||||
|
/> |
||||
|
</Modal> |
||||
|
{/if} |
||||
@ -0,0 +1,45 @@ |
|||||
|
<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 && !datasource?.entities[name] |
||||
|
$: error = |
||||
|
name && datasource?.entities[name] ? "Table name already in use." : null |
||||
|
|
||||
|
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:error bind:value={name} /> |
||||
|
</ModalContent> |
||||
@ -1,43 +0,0 @@ |
|||||
<script> |
|
||||
import { ModalContent, Select, Body } from "@budibase/bbui" |
|
||||
import { tables } from "stores/backend" |
|
||||
|
|
||||
export let datasource |
|
||||
export let plusTables |
|
||||
export let save |
|
||||
|
|
||||
async function saveDisplayColumns() { |
|
||||
// be explicit about copying over |
|
||||
for (let table of plusTables) { |
|
||||
datasource.entities[table.name].primaryDisplay = table.primaryDisplay |
|
||||
} |
|
||||
save() |
|
||||
await tables.fetch() |
|
||||
} |
|
||||
|
|
||||
function getColumnOptions(table) { |
|
||||
if (!table || !table.schema) { |
|
||||
return [] |
|
||||
} |
|
||||
return Object.entries(table.schema) |
|
||||
.filter(field => field[1].type !== "link") |
|
||||
.map(([fieldName]) => fieldName) |
|
||||
} |
|
||||
</script> |
|
||||
|
|
||||
<ModalContent |
|
||||
title="Edit display columns" |
|
||||
confirmText="Save" |
|
||||
onConfirm={saveDisplayColumns} |
|
||||
> |
|
||||
<Body |
|
||||
>Select the columns that will be shown when displaying relationships.</Body |
|
||||
> |
|
||||
{#each plusTables as table} |
|
||||
<Select |
|
||||
label={table.name} |
|
||||
options={getColumnOptions(table)} |
|
||||
bind:value={table.primaryDisplay} |
|
||||
/> |
|
||||
{/each} |
|
||||
</ModalContent> |
|
||||
@ -0,0 +1,276 @@ |
|||||
|
const CouchDB = require("../../../db") |
||||
|
const { |
||||
|
buildExternalTableId, |
||||
|
breakExternalTableId, |
||||
|
} = require("../../../integrations/utils") |
||||
|
const { |
||||
|
getTable, |
||||
|
generateForeignKey, |
||||
|
generateJunctionTableName, |
||||
|
foreignKeyStructure, |
||||
|
} = require("./utils") |
||||
|
const { |
||||
|
DataSourceOperation, |
||||
|
FieldTypes, |
||||
|
RelationshipTypes, |
||||
|
} = 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 cleanupRelationships(table, tables, oldTable = null) { |
||||
|
const tableToIterate = oldTable ? oldTable : table |
||||
|
// clean up relationships in couch table schemas
|
||||
|
for (let [key, schema] of Object.entries(tableToIterate.schema)) { |
||||
|
if ( |
||||
|
schema.type === FieldTypes.LINK && |
||||
|
(!oldTable || table.schema[key] == null) |
||||
|
) { |
||||
|
const relatedTable = Object.values(tables).find( |
||||
|
table => table._id === schema.tableId |
||||
|
) |
||||
|
const foreignKey = schema.foreignKey |
||||
|
if (!relatedTable || !foreignKey) { |
||||
|
continue |
||||
|
} |
||||
|
for (let [relatedKey, relatedSchema] of Object.entries( |
||||
|
relatedTable.schema |
||||
|
)) { |
||||
|
if ( |
||||
|
relatedSchema.type === FieldTypes.LINK && |
||||
|
relatedSchema.fieldName === foreignKey |
||||
|
) { |
||||
|
delete relatedTable.schema[relatedKey] |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function getDatasourceId(table) { |
||||
|
if (!table) { |
||||
|
throw "No table supplied" |
||||
|
} |
||||
|
if (table.sourceId) { |
||||
|
return table.sourceId |
||||
|
} |
||||
|
return breakExternalTableId(table._id).datasourceId |
||||
|
} |
||||
|
|
||||
|
function otherRelationshipType(type) { |
||||
|
if (type === RelationshipTypes.MANY_TO_MANY) { |
||||
|
return RelationshipTypes.MANY_TO_MANY |
||||
|
} |
||||
|
return type === RelationshipTypes.ONE_TO_MANY |
||||
|
? RelationshipTypes.MANY_TO_ONE |
||||
|
: RelationshipTypes.ONE_TO_MANY |
||||
|
} |
||||
|
|
||||
|
function generateManyLinkSchema(datasource, column, table, relatedTable) { |
||||
|
const primary = table.name + table.primary[0] |
||||
|
const relatedPrimary = relatedTable.name + relatedTable.primary[0] |
||||
|
const jcTblName = generateJunctionTableName(column, table, relatedTable) |
||||
|
// first create the new table
|
||||
|
const junctionTable = { |
||||
|
_id: buildExternalTableId(datasource._id, jcTblName), |
||||
|
name: jcTblName, |
||||
|
primary: [primary, relatedPrimary], |
||||
|
constrained: [primary, relatedPrimary], |
||||
|
schema: { |
||||
|
[primary]: foreignKeyStructure(primary, { |
||||
|
toTable: table.name, |
||||
|
toKey: table.primary[0], |
||||
|
}), |
||||
|
[relatedPrimary]: foreignKeyStructure(relatedPrimary, { |
||||
|
toTable: relatedTable.name, |
||||
|
toKey: relatedTable.primary[0], |
||||
|
}), |
||||
|
}, |
||||
|
} |
||||
|
column.through = junctionTable._id |
||||
|
column.throughFrom = primary |
||||
|
column.throughTo = relatedPrimary |
||||
|
column.fieldName = relatedPrimary |
||||
|
return junctionTable |
||||
|
} |
||||
|
|
||||
|
function generateLinkSchema(column, table, relatedTable, type) { |
||||
|
const isOneSide = type === RelationshipTypes.ONE_TO_MANY |
||||
|
const primary = isOneSide ? relatedTable.primary[0] : table.primary[0] |
||||
|
// generate a foreign key
|
||||
|
const foreignKey = generateForeignKey(column, relatedTable) |
||||
|
column.relationshipType = type |
||||
|
column.foreignKey = isOneSide ? foreignKey : primary |
||||
|
column.fieldName = isOneSide ? primary : foreignKey |
||||
|
return foreignKey |
||||
|
} |
||||
|
|
||||
|
function generateRelatedSchema(linkColumn, table, relatedTable, columnName) { |
||||
|
// generate column for other table
|
||||
|
const relatedSchema = cloneDeep(linkColumn) |
||||
|
// swap them from the main link
|
||||
|
if (linkColumn.foreignKey) { |
||||
|
relatedSchema.fieldName = linkColumn.foreignKey |
||||
|
relatedSchema.foreignKey = linkColumn.fieldName |
||||
|
} |
||||
|
// is many to many
|
||||
|
else { |
||||
|
// don't need to copy through, already got it
|
||||
|
relatedSchema.fieldName = linkColumn.throughFrom |
||||
|
relatedSchema.throughTo = linkColumn.throughFrom |
||||
|
relatedSchema.throughFrom = linkColumn.throughTo |
||||
|
} |
||||
|
relatedSchema.relationshipType = otherRelationshipType( |
||||
|
linkColumn.relationshipType |
||||
|
) |
||||
|
relatedSchema.tableId = relatedTable._id |
||||
|
relatedSchema.name = columnName |
||||
|
table.schema[columnName] = relatedSchema |
||||
|
} |
||||
|
|
||||
|
function isRelationshipSetup(column) { |
||||
|
return column.foreignKey || column.through |
||||
|
} |
||||
|
|
||||
|
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 oldTables = cloneDeep(datasource.entities) |
||||
|
const tables = datasource.entities |
||||
|
|
||||
|
const extraTablesToUpdate = [] |
||||
|
|
||||
|
// check if relations need setup
|
||||
|
for (let schema of Object.values(tableToSave.schema)) { |
||||
|
if (schema.type !== FieldTypes.LINK || isRelationshipSetup(schema)) { |
||||
|
continue |
||||
|
} |
||||
|
const relatedTable = Object.values(tables).find( |
||||
|
table => table._id === schema.tableId |
||||
|
) |
||||
|
const relatedColumnName = schema.fieldName |
||||
|
const relationType = schema.relationshipType |
||||
|
if (relationType === RelationshipTypes.MANY_TO_MANY) { |
||||
|
const junctionTable = generateManyLinkSchema( |
||||
|
datasource, |
||||
|
schema, |
||||
|
table, |
||||
|
relatedTable |
||||
|
) |
||||
|
if (tables[junctionTable.name]) { |
||||
|
throw "Junction table already exists, cannot create another relationship." |
||||
|
} |
||||
|
tables[junctionTable.name] = junctionTable |
||||
|
extraTablesToUpdate.push(junctionTable) |
||||
|
} else { |
||||
|
const fkTable = |
||||
|
relationType === RelationshipTypes.ONE_TO_MANY ? table : relatedTable |
||||
|
const foreignKey = generateLinkSchema( |
||||
|
schema, |
||||
|
table, |
||||
|
relatedTable, |
||||
|
relationType |
||||
|
) |
||||
|
fkTable.schema[foreignKey] = foreignKeyStructure(foreignKey) |
||||
|
if (fkTable.constrained == null) { |
||||
|
fkTable.constrained = [] |
||||
|
} |
||||
|
if (fkTable.constrained.indexOf(foreignKey) === -1) { |
||||
|
fkTable.constrained.push(foreignKey) |
||||
|
} |
||||
|
// foreign key is in other table, need to save it to external
|
||||
|
if (fkTable._id !== table._id) { |
||||
|
extraTablesToUpdate.push(fkTable) |
||||
|
} |
||||
|
} |
||||
|
generateRelatedSchema(schema, relatedTable, table, relatedColumnName) |
||||
|
schema.main = true |
||||
|
} |
||||
|
|
||||
|
cleanupRelationships(tableToSave, tables, oldTable) |
||||
|
|
||||
|
const operation = oldTable |
||||
|
? DataSourceOperation.UPDATE_TABLE |
||||
|
: DataSourceOperation.CREATE_TABLE |
||||
|
await makeTableRequest(datasource, operation, tableToSave, tables, oldTable) |
||||
|
// update any extra tables (like foreign keys in other tables)
|
||||
|
for (let extraTable of extraTablesToUpdate) { |
||||
|
const oldExtraTable = oldTables[extraTable.name] |
||||
|
let op = oldExtraTable |
||||
|
? DataSourceOperation.UPDATE_TABLE |
||||
|
: DataSourceOperation.CREATE_TABLE |
||||
|
await makeTableRequest(datasource, op, extraTable, tables, oldExtraTable) |
||||
|
} |
||||
|
|
||||
|
// make sure the constrained list, all still exist
|
||||
|
if (Array.isArray(tableToSave.constrained)) { |
||||
|
tableToSave.constrained = tableToSave.constrained.filter(constraint => |
||||
|
Object.keys(tableToSave.schema).includes(constraint) |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
// 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) |
||||
|
|
||||
|
cleanupRelationships(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,167 @@ |
|||||
|
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, RelationshipTypes } = 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 |
||||
|
const columns = Object.values(table.schema) |
||||
|
// all columns in a junction table will be meta
|
||||
|
let metaCols = columns.filter(col => col.meta) |
||||
|
let isJunction = metaCols.length === columns.length |
||||
|
// can't change primary once its set for now
|
||||
|
if (primaryKey && !oldTable && !isJunction) { |
||||
|
schema.increments(primaryKey).primary() |
||||
|
} else if (!oldTable && isJunction) { |
||||
|
schema.primary(metaCols.map(col => col.name)) |
||||
|
} |
||||
|
|
||||
|
|
||||
|
// check if any columns need added
|
||||
|
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 && !isJunction)) { |
||||
|
continue |
||||
|
} |
||||
|
switch (column.type) { |
||||
|
case FieldTypes.STRING: case FieldTypes.OPTIONS: case FieldTypes.LONGFORM: |
||||
|
schema.string(key) |
||||
|
break |
||||
|
case FieldTypes.NUMBER: |
||||
|
// if meta is specified then this is a junction table entry
|
||||
|
if (column.meta && column.meta.toKey && column.meta.toTable) { |
||||
|
const { toKey, toTable } = column.meta |
||||
|
schema.integer(key).unsigned() |
||||
|
schema.foreign(key).references(`${toTable}.${toKey}`) |
||||
|
} else 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: |
||||
|
// this side of the relationship doesn't need any SQL work
|
||||
|
if ( |
||||
|
column.relationshipType !== RelationshipTypes.MANY_TO_ONE && |
||||
|
column.relationshipType !== RelationshipTypes.MANY_TO_MANY |
||||
|
) { |
||||
|
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]}`) |
||||
|
} |
||||
|
break |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// need to check if any columns have been deleted
|
||||
|
if (oldTable) { |
||||
|
const deletedColumns = Object.entries(oldTable.schema) |
||||
|
.filter(([key, schema]) => schema.type !== FieldTypes.LINK && table.schema[key] == null) |
||||
|
.map(([key]) => key) |
||||
|
deletedColumns.forEach(key => { |
||||
|
if (oldTable.constrained && oldTable.constrained.indexOf(key) !== -1) { |
||||
|
schema.dropForeign(key) |
||||
|
} |
||||
|
schema.dropColumn(key) |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
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