budibase/packages/builder/src/stores/backend/datasources.js

240 lines
6.2 KiB
JavaScript

import { writable, derived, get } from "svelte/store"
import {
IntegrationTypes,
DEFAULT_BB_DATASOURCE_ID,
BUDIBASE_INTERNAL_DB_ID,
} from "constants/backend"
import { tables, queries } from "./"
import { API } from "api"
import { DatasourceFeature } from "@budibase/types"
import { TableNames } from "constants"
export class ImportTableError extends Error {
constructor(message) {
super(message)
const [title, description] = message.split(" - ")
this.name = "TableSelectionError"
// Capitalize the first character of both the title and description
this.title = title[0].toUpperCase() + title.substr(1)
this.description = description[0].toUpperCase() + description.substr(1)
}
}
export function createDatasourcesStore() {
const store = writable({
list: [],
selectedDatasourceId: null,
schemaError: null,
})
const derivedStore = derived([store, tables], ([$store, $tables]) => {
// Set the internal datasource entities from the table list, which we're
// able to keep updated unlike the egress generated definition of the
// internal datasource
let internalDS = $store.list?.find(ds => ds._id === BUDIBASE_INTERNAL_DB_ID)
let otherDS = $store.list?.filter(ds => ds._id !== BUDIBASE_INTERNAL_DB_ID)
if (internalDS) {
internalDS = {
...internalDS,
entities: $tables.list?.filter(table => {
return (
table.sourceId === BUDIBASE_INTERNAL_DB_ID &&
table._id !== TableNames.USERS
)
}),
}
}
// Build up enriched DS list
// Only add the internal DS if we have at least one non-users table
let list = []
if (internalDS?.entities?.length) {
list.push(internalDS)
}
list = list.concat(otherDS || [])
return {
...$store,
list,
selected: list?.find(ds => ds._id === $store.selectedDatasourceId),
hasDefaultData: list?.some(ds => ds._id === DEFAULT_BB_DATASOURCE_ID),
hasData: list?.length > 0,
}
})
const fetch = async () => {
const datasources = await API.getDatasources()
store.update(state => ({
...state,
list: datasources,
}))
}
const select = id => {
store.update(state => ({
...state,
selectedDatasourceId: id,
// Remove any possible schema error
schemaError: null,
}))
}
const updateDatasource = response => {
const { datasource, error } = response
if (error) {
store.update(state => ({
...state,
schemaError: error,
}))
}
replaceDatasource(datasource._id, datasource)
select(datasource._id)
return datasource
}
const updateSchema = async (datasource, tablesFilter) => {
try {
const response = await API.buildDatasourceSchema({
datasourceId: datasource?._id,
tablesFilter,
})
updateDatasource(response)
} catch (e) {
// buildDatasourceSchema call returns user presentable errors with two parts divided with a " - ".
if (e.message.split(" - ").length === 2) {
throw new ImportTableError(e.message)
} else {
throw e
}
}
}
const sourceCount = source => {
return get(store).list.filter(datasource => datasource.source === source)
.length
}
const checkDatasourceValidity = async (integration, datasource) => {
if (integration.features?.[DatasourceFeature.CONNECTION_CHECKING]) {
const { connected, error } = await API.validateDatasource(datasource)
if (connected) {
return
}
throw new Error(`Unable to connect: ${error}`)
}
}
const create = async ({ integration, config }) => {
const count = sourceCount(integration.name)
const nameModifier = count === 0 ? "" : ` ${count + 1}`
const datasource = {
type: "datasource",
source: integration.name,
config,
name: `${integration.friendlyName}${nameModifier}`,
plus: integration.plus && integration.name !== IntegrationTypes.REST,
isSQL: integration.isSQL,
}
if (await checkDatasourceValidity(integration, datasource)) {
throw new Error("Unable to connect")
}
const response = await API.createDatasource({
datasource,
fetchSchema: integration.plus,
})
return updateDatasource(response)
}
const update = async ({ integration, datasource }) => {
if (await checkDatasourceValidity(integration, datasource)) {
throw new Error("Unable to connect")
}
const response = await API.updateDatasource(datasource)
return updateDatasource(response)
}
const deleteDatasource = async datasource => {
if (!datasource?._id || !datasource?._rev) {
return
}
await API.deleteDatasource({
datasourceId: datasource._id,
datasourceRev: datasource._rev,
})
replaceDatasource(datasource._id, null)
}
const removeSchemaError = () => {
store.update(state => {
return { ...state, schemaError: null }
})
}
const replaceDatasource = (datasourceId, datasource) => {
if (!datasourceId) {
return
}
// Handle deletion
if (!datasource) {
store.update(state => ({
...state,
list: state.list.filter(x => x._id !== datasourceId),
}))
tables.removeDatasourceTables(datasourceId)
queries.removeDatasourceQueries(datasourceId)
return
}
// Add new datasource
const index = get(store).list.findIndex(x => x._id === datasource._id)
if (index === -1) {
store.update(state => ({
...state,
list: [...state.list, datasource],
}))
// If this is a new datasource then we should refresh the tables list,
// because otherwise we'll never see the new tables
tables.fetch()
}
// Update existing datasource
else if (datasource) {
store.update(state => {
state.list[index] = datasource
return state
})
}
}
const getTableNames = async datasource => {
const info = await API.fetchInfoForDatasource(datasource)
return info.tableNames || []
}
return {
subscribe: derivedStore.subscribe,
fetch,
init: fetch,
select,
updateSchema,
create,
update,
delete: deleteDatasource,
removeSchemaError,
replaceDatasource,
getTableNames,
}
}
export const datasources = createDatasourcesStore()