mirror of https://github.com/Budibase/budibase.git
committed by
GitHub
142 changed files with 7343 additions and 4632 deletions
@ -0,0 +1,239 @@ |
|||
<script> |
|||
import { createEventDispatcher } from "svelte" |
|||
import "@spectrum-css/popover/dist/index-vars.css" |
|||
import clickOutside from "../Actions/click_outside" |
|||
import { fly } from "svelte/transition" |
|||
import Icon from "../Icon/Icon.svelte" |
|||
import Input from "../Form/Input.svelte" |
|||
import { capitalise } from "../utils/helpers" |
|||
|
|||
export let value |
|||
export let size = "M" |
|||
|
|||
let open = false |
|||
|
|||
$: color = value || "transparent" |
|||
$: customValue = getCustomValue(value) |
|||
$: checkColor = getCheckColor(value) |
|||
|
|||
const dispatch = createEventDispatcher() |
|||
const categories = [ |
|||
{ |
|||
label: "Grays", |
|||
colors: [ |
|||
"white", |
|||
"gray-100", |
|||
"gray-200", |
|||
"gray-300", |
|||
"gray-400", |
|||
"gray-500", |
|||
"gray-600", |
|||
"gray-700", |
|||
"gray-800", |
|||
"gray-900", |
|||
"black", |
|||
], |
|||
}, |
|||
{ |
|||
label: "Colors", |
|||
colors: [ |
|||
"red-400", |
|||
"orange-400", |
|||
"yellow-400", |
|||
"green-400", |
|||
"seafoam-400", |
|||
"blue-400", |
|||
"indigo-400", |
|||
"magenta-400", |
|||
|
|||
"red-500", |
|||
"orange-500", |
|||
"yellow-500", |
|||
"green-500", |
|||
"seafoam-500", |
|||
"blue-500", |
|||
"indigo-500", |
|||
"magenta-500", |
|||
|
|||
"red-600", |
|||
"orange-600", |
|||
"yellow-600", |
|||
"green-600", |
|||
"seafoam-600", |
|||
"blue-600", |
|||
"indigo-600", |
|||
"magenta-600", |
|||
|
|||
"red-700", |
|||
"orange-700", |
|||
"yellow-700", |
|||
"green-700", |
|||
"seafoam-700", |
|||
"blue-700", |
|||
"indigo-700", |
|||
"magenta-700", |
|||
], |
|||
}, |
|||
] |
|||
|
|||
const onChange = value => { |
|||
dispatch("change", value) |
|||
open = false |
|||
} |
|||
|
|||
const getCustomValue = value => { |
|||
if (!value) { |
|||
return value |
|||
} |
|||
let found = false |
|||
const comparisonValue = value.substring(35, value.length - 1) |
|||
for (let category of categories) { |
|||
found = category.colors.includes(comparisonValue) |
|||
if (found) { |
|||
break |
|||
} |
|||
} |
|||
return found ? null : value |
|||
} |
|||
|
|||
const prettyPrint = color => { |
|||
return capitalise(color.split("-").join(" ")) |
|||
} |
|||
|
|||
const getCheckColor = value => { |
|||
return /^.*(white|(gray-(50|75|100|200|300|400|500)))\)$/.test(value) |
|||
? "black" |
|||
: "white" |
|||
} |
|||
</script> |
|||
|
|||
<div class="container"> |
|||
<div |
|||
class="preview size--{size || 'M'}" |
|||
style="background: {color};" |
|||
on:click={() => (open = true)} |
|||
/> |
|||
{#if open} |
|||
<div |
|||
use:clickOutside={() => (open = false)} |
|||
transition:fly={{ y: -20, duration: 200 }} |
|||
class="spectrum-Popover spectrum-Popover--bottom spectrum-Picker-popover is-open" |
|||
> |
|||
{#each categories as category} |
|||
<div class="category"> |
|||
<div class="heading">{category.label}</div> |
|||
<div class="colors"> |
|||
{#each category.colors as color} |
|||
<div |
|||
on:click={() => { |
|||
onChange(`var(--spectrum-global-color-static-${color})`) |
|||
}} |
|||
class="color" |
|||
style="background: var(--spectrum-global-color-static-{color}); color: {checkColor};" |
|||
title={prettyPrint(color)} |
|||
> |
|||
{#if value === `var(--spectrum-global-color-static-${color})`} |
|||
<Icon name="Checkmark" size="S" /> |
|||
{/if} |
|||
</div> |
|||
{/each} |
|||
</div> |
|||
</div> |
|||
{/each} |
|||
<div class="category category--custom"> |
|||
<div class="heading">Custom</div> |
|||
<div class="custom"> |
|||
<Input |
|||
updateOnChange={false} |
|||
quiet |
|||
placeholder="Hex, RGB, HSL..." |
|||
value={customValue} |
|||
on:change |
|||
/> |
|||
<Icon |
|||
size="S" |
|||
name="Close" |
|||
hoverable |
|||
on:click={() => onChange(null)} |
|||
/> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
{/if} |
|||
</div> |
|||
|
|||
<style> |
|||
.container { |
|||
position: relative; |
|||
} |
|||
.preview { |
|||
width: 32px; |
|||
height: 32px; |
|||
border-radius: 100%; |
|||
transition: border-color 130ms ease-in-out; |
|||
box-shadow: 0 0 0 1px var(--spectrum-global-color-gray-300); |
|||
} |
|||
.preview:hover { |
|||
cursor: pointer; |
|||
box-shadow: 0 0 2px 2px var(--spectrum-global-color-gray-300); |
|||
} |
|||
.size--S { |
|||
width: 20px; |
|||
height: 20px; |
|||
} |
|||
.size--M { |
|||
width: 32px; |
|||
height: 32px; |
|||
} |
|||
.size--L { |
|||
width: 48px; |
|||
height: 48px; |
|||
} |
|||
.spectrum-Popover { |
|||
width: 210px; |
|||
z-index: 999; |
|||
top: 100%; |
|||
padding: var(--spacing-l) var(--spacing-xl); |
|||
display: flex; |
|||
flex-direction: column; |
|||
justify-content: flex-start; |
|||
align-items: stretch; |
|||
gap: var(--spacing-xl); |
|||
} |
|||
.colors { |
|||
display: grid; |
|||
grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr; |
|||
gap: var(--spacing-xs); |
|||
} |
|||
.heading { |
|||
font-size: var(--font-size-s); |
|||
font-weight: 600; |
|||
letter-spacing: 0.14px; |
|||
flex: 1 1 auto; |
|||
text-transform: uppercase; |
|||
grid-column: 1 / 5; |
|||
margin-bottom: var(--spacing-s); |
|||
} |
|||
.color { |
|||
height: 16px; |
|||
width: 16px; |
|||
border-radius: 100%; |
|||
box-shadow: 0 0 0 1px var(--spectrum-global-color-gray-300); |
|||
display: grid; |
|||
place-items: center; |
|||
} |
|||
.color:hover { |
|||
cursor: pointer; |
|||
box-shadow: 0 0 2px 2px var(--spectrum-global-color-gray-300); |
|||
} |
|||
.custom { |
|||
display: grid; |
|||
grid-template-columns: 1fr auto; |
|||
align-items: center; |
|||
gap: var(--spacing-m); |
|||
margin-right: var(--spacing-xs); |
|||
} |
|||
.category--custom .heading { |
|||
margin-bottom: var(--spacing-xs); |
|||
} |
|||
</style> |
|||
@ -1,40 +0,0 @@ |
|||
<script> |
|||
export let categories = [] |
|||
export let selectedCategory = {} |
|||
export let onClick = () => {} |
|||
</script> |
|||
|
|||
<div class="tabs"> |
|||
{#each categories as category} |
|||
<li |
|||
data-cy={category.name} |
|||
on:click={() => onClick(category)} |
|||
class:active={selectedCategory === category} |
|||
> |
|||
{category.name} |
|||
</li> |
|||
{/each} |
|||
</div> |
|||
|
|||
<style> |
|||
.tabs { |
|||
display: flex; |
|||
flex-direction: row; |
|||
justify-content: flex-start; |
|||
align-items: center; |
|||
list-style: none; |
|||
font-size: var(--font-size-m); |
|||
font-weight: 600; |
|||
height: 24px; |
|||
} |
|||
|
|||
li { |
|||
color: var(--grey-5); |
|||
cursor: pointer; |
|||
margin-right: 20px; |
|||
} |
|||
|
|||
.active { |
|||
color: var(--ink); |
|||
} |
|||
</style> |
|||
@ -0,0 +1,60 @@ |
|||
<script> |
|||
import { |
|||
TextArea, |
|||
DetailSummary, |
|||
ActionButton, |
|||
Drawer, |
|||
DrawerContent, |
|||
Layout, |
|||
Body, |
|||
Button, |
|||
} from "@budibase/bbui" |
|||
import { store } from "builderStore" |
|||
|
|||
export let componentInstance |
|||
|
|||
let tempValue |
|||
let drawer |
|||
|
|||
const openDrawer = () => { |
|||
tempValue = componentInstance?._styles?.custom |
|||
drawer.show() |
|||
} |
|||
|
|||
const save = () => { |
|||
store.actions.components.updateCustomStyle(tempValue) |
|||
drawer.hide() |
|||
} |
|||
</script> |
|||
|
|||
<DetailSummary |
|||
name={`Custom CSS${componentInstance?._styles?.custom ? " *" : ""}`} |
|||
collapsible={false} |
|||
> |
|||
<div> |
|||
<ActionButton on:click={openDrawer}>Edit custom CSS</ActionButton> |
|||
</div> |
|||
</DetailSummary> |
|||
<Drawer bind:this={drawer} title="Custom CSS"> |
|||
<Button cta slot="buttons" on:click={save}>Save</Button> |
|||
<DrawerContent slot="body"> |
|||
<div class="content"> |
|||
<Layout gap="S"> |
|||
<Body size="S">Custom CSS overrides all other component styles.</Body> |
|||
<TextArea bind:value={tempValue} placeholder="Enter some CSS..." /> |
|||
</Layout> |
|||
</div> |
|||
</DrawerContent> |
|||
</Drawer> |
|||
|
|||
<style> |
|||
.content { |
|||
max-width: 800px; |
|||
margin: 0 auto; |
|||
} |
|||
.content :global(textarea) { |
|||
font-family: monospace; |
|||
min-height: 240px !important; |
|||
font-size: var(--font-size-s); |
|||
} |
|||
</style> |
|||
@ -0,0 +1,34 @@ |
|||
<script> |
|||
import StyleSection from "./StyleSection.svelte" |
|||
import * as ComponentStyles from "./componentStyles" |
|||
|
|||
export let componentDefinition |
|||
export let componentInstance |
|||
|
|||
const getStyles = def => { |
|||
if (!def?.styles?.length) { |
|||
return [...ComponentStyles.all] |
|||
} |
|||
let styles = [...ComponentStyles.all] |
|||
def.styles.forEach(style => { |
|||
if (ComponentStyles[style]) { |
|||
styles.push(ComponentStyles[style]) |
|||
} |
|||
}) |
|||
return styles |
|||
} |
|||
|
|||
$: styles = getStyles(componentDefinition) |
|||
</script> |
|||
|
|||
{#if styles?.length > 0} |
|||
{#each styles as style} |
|||
<StyleSection |
|||
{style} |
|||
name={style.label} |
|||
columns={style.columns} |
|||
properties={style.settings} |
|||
{componentInstance} |
|||
/> |
|||
{/each} |
|||
{/if} |
|||
@ -1,111 +0,0 @@ |
|||
<script> |
|||
import { TextArea, DetailSummary, Button } from "@budibase/bbui" |
|||
import PropertyGroup from "./PropertyControls/PropertyGroup.svelte" |
|||
import FlatButtonGroup from "./PropertyControls/FlatButtonGroup" |
|||
import { allStyles } from "./componentStyles" |
|||
|
|||
export let componentDefinition = {} |
|||
export let componentInstance = {} |
|||
export let onStyleChanged = () => {} |
|||
export let onCustomStyleChanged = () => {} |
|||
export let onResetStyles = () => {} |
|||
|
|||
let selectedCategory = "normal" |
|||
let currentGroup |
|||
|
|||
function onChange(category) { |
|||
selectedCategory = category |
|||
} |
|||
|
|||
const buttonProps = [ |
|||
{ value: "normal", text: "Normal" }, |
|||
{ value: "hover", text: "Hover" }, |
|||
{ value: "active", text: "Active" }, |
|||
] |
|||
|
|||
$: groups = componentDefinition?.styleable ? Object.keys(allStyles) : [] |
|||
</script> |
|||
|
|||
<div class="container"> |
|||
<div class="state-categories"> |
|||
<FlatButtonGroup value={selectedCategory} {buttonProps} {onChange} /> |
|||
</div> |
|||
|
|||
<div class="positioned-wrapper"> |
|||
<div class="property-groups"> |
|||
{#if groups.length > 0} |
|||
{#each groups as groupName} |
|||
<PropertyGroup |
|||
name={groupName} |
|||
properties={allStyles[groupName]} |
|||
styleCategory={selectedCategory} |
|||
{onStyleChanged} |
|||
{componentInstance} |
|||
open={currentGroup === groupName} |
|||
on:open={() => (currentGroup = groupName)} |
|||
/> |
|||
{/each} |
|||
<DetailSummary |
|||
name={`Custom Styles${componentInstance._styles.custom ? " *" : ""}`} |
|||
on:open={() => (currentGroup = "custom")} |
|||
show={currentGroup === "custom"} |
|||
thin |
|||
> |
|||
<div class="custom-styles"> |
|||
<TextArea |
|||
value={componentInstance._styles.custom} |
|||
on:change={event => onCustomStyleChanged(event.detail)} |
|||
placeholder="Enter some CSS..." |
|||
/> |
|||
</div> |
|||
</DetailSummary> |
|||
<Button secondary wide on:click={onResetStyles}>Reset Styles</Button> |
|||
{:else} |
|||
<div class="no-design"> |
|||
This component doesn't have any design properties. |
|||
</div> |
|||
{/if} |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<style> |
|||
.container { |
|||
display: flex; |
|||
flex-direction: column; |
|||
width: 100%; |
|||
height: 100%; |
|||
gap: var(--spacing-l); |
|||
} |
|||
|
|||
.positioned-wrapper { |
|||
position: relative; |
|||
display: flex; |
|||
min-height: 0; |
|||
flex: 1 1 auto; |
|||
} |
|||
|
|||
.property-groups { |
|||
flex: 1; |
|||
overflow-y: auto; |
|||
min-height: 0; |
|||
margin: 0 -20px; |
|||
padding: 0 20px; |
|||
display: flex; |
|||
flex-direction: column; |
|||
justify-content: flex-start; |
|||
align-items: stretch; |
|||
gap: var(--spacing-m); |
|||
} |
|||
|
|||
.no-design { |
|||
font-size: var(--font-size-xs); |
|||
color: var(--grey-5); |
|||
} |
|||
|
|||
.custom-styles :global(textarea) { |
|||
font-family: monospace; |
|||
min-height: 120px; |
|||
font-size: var(--font-size-xs); |
|||
} |
|||
</style> |
|||
@ -1,84 +1,33 @@ |
|||
<script> |
|||
import { get } from "svelte/store" |
|||
import { store, selectedComponent, currentAsset } from "builderStore" |
|||
import { store, selectedComponent } from "builderStore" |
|||
import { Tabs, Tab } from "@budibase/bbui" |
|||
import { FrontendTypes } from "constants" |
|||
import DesignView from "./DesignView.svelte" |
|||
import SettingsView from "./SettingsView.svelte" |
|||
import { setWith } from "lodash" |
|||
import ScreenSettingsSection from "./ScreenSettingsSection.svelte" |
|||
import ComponentSettingsSection from "./ComponentSettingsSection.svelte" |
|||
import DesignSection from "./DesignSection.svelte" |
|||
import CustomStylesSection from "./CustomStylesSection.svelte" |
|||
|
|||
$: definition = store.actions.components.getDefinition( |
|||
$: componentInstance = $selectedComponent |
|||
$: componentDefinition = store.actions.components.getDefinition( |
|||
$selectedComponent?._component |
|||
) |
|||
$: isComponentOrScreen = |
|||
$store.currentView === "component" || |
|||
$store.currentFrontEndType === FrontendTypes.SCREEN |
|||
$: isNotScreenslot = !$selectedComponent._component.endsWith("screenslot") |
|||
$: showDisplayName = isComponentOrScreen && isNotScreenslot |
|||
|
|||
const onStyleChanged = store.actions.components.updateStyle |
|||
const onCustomStyleChanged = store.actions.components.updateCustomStyle |
|||
const onResetStyles = store.actions.components.resetStyles |
|||
|
|||
function setAssetProps(name, value) { |
|||
const selectedAsset = get(currentAsset) |
|||
store.update(state => { |
|||
if ( |
|||
name === "_instanceName" && |
|||
state.currentFrontEndType === FrontendTypes.SCREEN |
|||
) { |
|||
selectedAsset.props._instanceName = value |
|||
} else { |
|||
setWith(selectedAsset, name.split("."), value, Object) |
|||
} |
|||
return state |
|||
}) |
|||
store.actions.preview.saveSelected() |
|||
} |
|||
</script> |
|||
|
|||
<Tabs selected="Settings"> |
|||
<Tabs selected="Settings" noPadding> |
|||
<Tab title="Settings"> |
|||
<div class="tab-content-padding"> |
|||
{#if definition && definition.name} |
|||
<div class="instance-name">{definition.name}</div> |
|||
{/if} |
|||
<SettingsView |
|||
componentInstance={$selectedComponent} |
|||
componentDefinition={definition} |
|||
{showDisplayName} |
|||
onChange={store.actions.components.updateProp} |
|||
onScreenPropChange={setAssetProps} |
|||
assetInstance={$store.currentView !== "component" && $currentAsset} |
|||
/> |
|||
</div> |
|||
</Tab> |
|||
<Tab title="Design"> |
|||
<div class="tab-content-padding"> |
|||
{#if definition && definition.name} |
|||
<div class="instance-name">{definition.name}</div> |
|||
{/if} |
|||
<DesignView |
|||
componentInstance={$selectedComponent} |
|||
componentDefinition={definition} |
|||
{onStyleChanged} |
|||
{onCustomStyleChanged} |
|||
{onResetStyles} |
|||
/> |
|||
<div class="container"> |
|||
<ScreenSettingsSection {componentInstance} {componentDefinition} /> |
|||
<ComponentSettingsSection {componentInstance} {componentDefinition} /> |
|||
<DesignSection {componentInstance} {componentDefinition} /> |
|||
<CustomStylesSection {componentInstance} {componentDefinition} /> |
|||
</div> |
|||
</Tab> |
|||
</Tabs> |
|||
|
|||
<style> |
|||
.tab-content-padding { |
|||
padding: 0 var(--spacing-xl); |
|||
} |
|||
|
|||
.instance-name { |
|||
font-size: var(--spectrum-global-dimension-font-size-75); |
|||
margin-bottom: var(--spacing-m); |
|||
margin-top: var(--spacing-xs); |
|||
font-weight: 600; |
|||
color: var(--grey-7); |
|||
.container { |
|||
display: flex; |
|||
flex-direction: column; |
|||
justify-content: flex-start; |
|||
align-items: stretch; |
|||
} |
|||
</style> |
|||
|
|||
@ -1,54 +0,0 @@ |
|||
<script> |
|||
import PropertyControl from "./PropertyControl.svelte" |
|||
import { DetailSummary } from "@budibase/bbui" |
|||
|
|||
export let name = "" |
|||
export let styleCategory = "normal" |
|||
export let properties = [] |
|||
export let componentInstance = {} |
|||
export let onStyleChanged = () => {} |
|||
export let open = false |
|||
|
|||
$: style = componentInstance["_styles"][styleCategory] || {} |
|||
$: changed = properties.some(prop => hasPropChanged(style, prop)) |
|||
|
|||
const hasPropChanged = (style, prop) => { |
|||
return style[prop.key] != null && style[prop.key] !== "" |
|||
} |
|||
|
|||
const getControlProps = props => { |
|||
let controlProps = { ...(props || {}) } |
|||
delete controlProps.label |
|||
delete controlProps.key |
|||
delete controlProps.control |
|||
return controlProps |
|||
} |
|||
</script> |
|||
|
|||
<DetailSummary name={`${name}${changed ? " *" : ""}`} on:open show={open} thin> |
|||
{#if open} |
|||
<div> |
|||
{#each properties as prop (`${componentInstance._id}-${prop.key}-${prop.label}`)} |
|||
<PropertyControl |
|||
bindable={false} |
|||
label={`${prop.label}${hasPropChanged(style, prop) ? " *" : ""}`} |
|||
control={prop.control} |
|||
key={prop.key} |
|||
value={style[prop.key]} |
|||
onChange={value => onStyleChanged(styleCategory, prop.key, value)} |
|||
props={getControlProps(prop)} |
|||
/> |
|||
{/each} |
|||
</div> |
|||
{/if} |
|||
</DetailSummary> |
|||
|
|||
<style> |
|||
div { |
|||
display: flex; |
|||
flex-direction: column; |
|||
justify-content: flex-start; |
|||
align-items: stretch; |
|||
gap: var(--spacing-s); |
|||
} |
|||
</style> |
|||
@ -0,0 +1,43 @@ |
|||
<script> |
|||
import { ActionButton } from "@budibase/bbui" |
|||
import { currentAsset, store } from "builderStore" |
|||
import { findClosestMatchingComponent } from "builderStore/storeUtils" |
|||
import { makeDatasourceFormComponents } from "builderStore/store/screenTemplates/utils/commonComponents" |
|||
import ConfirmDialog from "components/common/ConfirmDialog.svelte" |
|||
|
|||
export let componentInstance |
|||
|
|||
let confirmResetFieldsDialog |
|||
|
|||
const resetFormFields = () => { |
|||
const form = findClosestMatchingComponent( |
|||
$currentAsset.props, |
|||
componentInstance._id, |
|||
component => component._component.endsWith("/form") |
|||
) |
|||
const dataSource = form?.dataSource |
|||
const fields = makeDatasourceFormComponents(dataSource) |
|||
store.actions.components.updateProp( |
|||
"_children", |
|||
fields.map(field => field.json()) |
|||
) |
|||
} |
|||
</script> |
|||
|
|||
<div> |
|||
<ActionButton |
|||
secondary |
|||
wide |
|||
on:click={() => confirmResetFieldsDialog?.show()} |
|||
> |
|||
Update form fields |
|||
</ActionButton> |
|||
</div> |
|||
|
|||
<ConfirmDialog |
|||
bind:this={confirmResetFieldsDialog} |
|||
body={`All components inside this group will be deleted and replaced with fields to match the schema. Are you sure you want to update this Field Group?`} |
|||
okText="Update" |
|||
onOk={resetFormFields} |
|||
title="Confirm Form Field Update" |
|||
/> |
|||
@ -0,0 +1,50 @@ |
|||
<script> |
|||
import { get } from "svelte/store" |
|||
import { get as deepGet, setWith } from "lodash" |
|||
import { Input, DetailSummary } from "@budibase/bbui" |
|||
import PropertyControl from "./PropertyControls/PropertyControl.svelte" |
|||
import LayoutSelect from "./PropertyControls/LayoutSelect.svelte" |
|||
import RoleSelect from "./PropertyControls/RoleSelect.svelte" |
|||
import { currentAsset, store } from "builderStore" |
|||
import { FrontendTypes } from "constants" |
|||
|
|||
export let componentInstance |
|||
|
|||
function setAssetProps(name, value) { |
|||
const selectedAsset = get(currentAsset) |
|||
store.update(state => { |
|||
if ( |
|||
name === "_instanceName" && |
|||
state.currentFrontEndType === FrontendTypes.SCREEN |
|||
) { |
|||
selectedAsset.props._instanceName = value |
|||
} else { |
|||
setWith(selectedAsset, name.split("."), value, Object) |
|||
} |
|||
return state |
|||
}) |
|||
store.actions.preview.saveSelected() |
|||
} |
|||
|
|||
const screenSettings = [ |
|||
// { key: "description", label: "Description", control: Input }, |
|||
{ key: "routing.route", label: "Route", control: Input }, |
|||
{ key: "routing.roleId", label: "Access", control: RoleSelect }, |
|||
{ key: "layoutId", label: "Layout", control: LayoutSelect }, |
|||
] |
|||
</script> |
|||
|
|||
{#if $store.currentView !== "component" && $currentAsset && $store.currentFrontEndType === FrontendTypes.SCREEN} |
|||
<DetailSummary name="Screen" collapsible={false}> |
|||
{#each screenSettings as def (`${componentInstance._id}-${def.key}`)} |
|||
<PropertyControl |
|||
bindable={false} |
|||
control={def.control} |
|||
label={def.label} |
|||
key={def.key} |
|||
value={deepGet($currentAsset, def.key)} |
|||
onChange={val => setAssetProps(def.key, val)} |
|||
/> |
|||
{/each} |
|||
</DetailSummary> |
|||
{/if} |
|||
@ -0,0 +1,51 @@ |
|||
<script> |
|||
import PropertyControl from "./PropertyControls/PropertyControl.svelte" |
|||
import { DetailSummary } from "@budibase/bbui" |
|||
import { store } from "builderStore" |
|||
|
|||
export let name |
|||
export let columns |
|||
export let properties |
|||
export let componentInstance |
|||
|
|||
$: style = componentInstance._styles.normal || {} |
|||
$: changed = properties?.some(prop => hasPropChanged(style, prop)) ?? false |
|||
|
|||
const hasPropChanged = (style, prop) => { |
|||
return style[prop.key] != null && style[prop.key] !== "" |
|||
} |
|||
|
|||
const getControlProps = props => { |
|||
let controlProps = { ...(props || {}) } |
|||
delete controlProps.label |
|||
delete controlProps.key |
|||
delete controlProps.control |
|||
return controlProps |
|||
} |
|||
</script> |
|||
|
|||
<DetailSummary collapsible={false} name={`${name}${changed ? " *" : ""}`}> |
|||
<div class="group-content" style="grid-template-columns: {columns || '1fr'}"> |
|||
{#each properties as prop (`${componentInstance._id}-${prop.key}-${prop.label}`)} |
|||
<div style="grid-column: {prop.column || 'auto'}"> |
|||
<PropertyControl |
|||
bindable={false} |
|||
label={`${prop.label}${hasPropChanged(style, prop) ? " *" : ""}`} |
|||
control={prop.control} |
|||
key={prop.key} |
|||
value={style[prop.key]} |
|||
onChange={val => store.actions.components.updateStyle(prop.key, val)} |
|||
props={getControlProps(prop)} |
|||
/> |
|||
</div> |
|||
{/each} |
|||
</div> |
|||
</DetailSummary> |
|||
|
|||
<style> |
|||
.group-content { |
|||
display: grid; |
|||
align-items: stretch; |
|||
gap: var(--spacing-l); |
|||
} |
|||
</style> |
|||
File diff suppressed because it is too large
File diff suppressed because it is too large
@ -0,0 +1,26 @@ |
|||
<script> |
|||
import { ColorPicker } from "@budibase/bbui" |
|||
import { builderStore } from "../../store" |
|||
|
|||
export let prop |
|||
|
|||
$: currentValue = $builderStore.selectedComponent?.[prop] |
|||
</script> |
|||
|
|||
<div> |
|||
<ColorPicker |
|||
size="S" |
|||
value={currentValue} |
|||
on:change={e => { |
|||
if (prop) { |
|||
builderStore.actions.updateProp(prop, e.detail) |
|||
} |
|||
}} |
|||
/> |
|||
</div> |
|||
|
|||
<style> |
|||
div { |
|||
padding: 0 4px; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,31 @@ |
|||
<script> |
|||
import { Select } from "@budibase/bbui" |
|||
import { builderStore } from "../../store" |
|||
|
|||
export let prop |
|||
export let options |
|||
export let label |
|||
|
|||
$: currentValue = $builderStore.selectedComponent?.[prop] |
|||
</script> |
|||
|
|||
<div> |
|||
<Select |
|||
quiet |
|||
autoWidth |
|||
placeholder={label} |
|||
{options} |
|||
value={currentValue} |
|||
on:change={e => { |
|||
if (prop) { |
|||
builderStore.actions.updateProp(prop, e.detail) |
|||
} |
|||
}} |
|||
/> |
|||
</div> |
|||
|
|||
<style> |
|||
div { |
|||
padding: 0 4px; |
|||
} |
|||
</style> |
|||
@ -1,4 +1,4 @@ |
|||
const elastic = {} |
|||
const elastic: any = {} |
|||
|
|||
elastic.Client = function () { |
|||
this.index = jest.fn().mockResolvedValue({ body: [] }) |
|||
@ -1,18 +0,0 @@ |
|||
class Email { |
|||
constructor() { |
|||
this.apiKey = null |
|||
} |
|||
|
|||
setApiKey(apiKey) { |
|||
this.apiKey = apiKey |
|||
} |
|||
|
|||
async send(msg) { |
|||
if (msg.to === "invalid@test.com") { |
|||
throw "Invalid" |
|||
} |
|||
return msg |
|||
} |
|||
} |
|||
|
|||
module.exports = new Email() |
|||
@ -0,0 +1,22 @@ |
|||
module SendgridMock { |
|||
class Email { |
|||
constructor() { |
|||
// @ts-ignore
|
|||
this.apiKey = null |
|||
} |
|||
|
|||
setApiKey(apiKey: any) { |
|||
// @ts-ignore
|
|||
this.apiKey = apiKey |
|||
} |
|||
|
|||
async send(msg: any) { |
|||
if (msg.to === "invalid@test.com") { |
|||
throw "Invalid" |
|||
} |
|||
return msg |
|||
} |
|||
} |
|||
|
|||
module.exports = new Email() |
|||
} |
|||
@ -1,5 +0,0 @@ |
|||
function Airtable() { |
|||
this.base = jest.fn() |
|||
} |
|||
|
|||
module.exports = Airtable |
|||
@ -0,0 +1,8 @@ |
|||
module AirtableMock { |
|||
function Airtable() { |
|||
// @ts-ignore
|
|||
this.base = jest.fn() |
|||
} |
|||
|
|||
module.exports = Airtable |
|||
} |
|||
@ -1,21 +0,0 @@ |
|||
const arangodb = {} |
|||
|
|||
arangodb.Database = function () { |
|||
this.query = jest.fn(() => ({ |
|||
all: jest.fn(), |
|||
})) |
|||
this.collection = jest.fn(() => "collection") |
|||
this.close = jest.fn() |
|||
} |
|||
|
|||
arangodb.aql = (strings, ...args) => { |
|||
let str = strings.join("{}") |
|||
|
|||
for (let arg of args) { |
|||
str = str.replace("{}", arg) |
|||
} |
|||
|
|||
return str |
|||
} |
|||
|
|||
module.exports = arangodb |
|||
@ -0,0 +1,24 @@ |
|||
module ArangoMock { |
|||
const arangodb: any = {} |
|||
|
|||
arangodb.Database = function () { |
|||
this.query = jest.fn(() => ({ |
|||
all: jest.fn(), |
|||
})) |
|||
this.collection = jest.fn(() => "collection") |
|||
this.close = jest.fn() |
|||
} |
|||
|
|||
// @ts-ignore
|
|||
arangodb.aql = (strings, ...args) => { |
|||
let str = strings.join("{}") |
|||
|
|||
for (let arg of args) { |
|||
str = str.replace("{}", arg) |
|||
} |
|||
|
|||
return str |
|||
} |
|||
|
|||
module.exports = arangodb |
|||
} |
|||
@ -1,38 +0,0 @@ |
|||
const aws = {} |
|||
|
|||
const response = body => () => ({ promise: () => body }) |
|||
|
|||
function DocumentClient() { |
|||
this.put = jest.fn(response({})) |
|||
this.query = jest.fn( |
|||
response({ |
|||
Items: [], |
|||
}) |
|||
) |
|||
this.scan = jest.fn( |
|||
response({ |
|||
Items: [ |
|||
{ |
|||
Name: "test", |
|||
}, |
|||
], |
|||
}) |
|||
) |
|||
this.get = jest.fn(response({})) |
|||
this.update = jest.fn(response({})) |
|||
this.delete = jest.fn(response({})) |
|||
} |
|||
|
|||
function S3() { |
|||
this.listObjects = jest.fn( |
|||
response({ |
|||
Contents: {}, |
|||
}) |
|||
) |
|||
} |
|||
|
|||
aws.DynamoDB = { DocumentClient } |
|||
aws.S3 = S3 |
|||
aws.config = { update: jest.fn() } |
|||
|
|||
module.exports = aws |
|||
@ -0,0 +1,47 @@ |
|||
module AwsMock { |
|||
const aws: any = {} |
|||
|
|||
const response = (body: any) => () => ({promise: () => body}) |
|||
|
|||
function DocumentClient() { |
|||
// @ts-ignore
|
|||
this.put = jest.fn(response({})) |
|||
// @ts-ignore
|
|||
this.query = jest.fn( |
|||
response({ |
|||
Items: [], |
|||
}) |
|||
) |
|||
// @ts-ignore
|
|||
this.scan = jest.fn( |
|||
response({ |
|||
Items: [ |
|||
{ |
|||
Name: "test", |
|||
}, |
|||
], |
|||
}) |
|||
) |
|||
// @ts-ignore
|
|||
this.get = jest.fn(response({})) |
|||
// @ts-ignore
|
|||
this.update = jest.fn(response({})) |
|||
// @ts-ignore
|
|||
this.delete = jest.fn(response({})) |
|||
} |
|||
|
|||
function S3() { |
|||
// @ts-ignore
|
|||
this.listObjects = jest.fn( |
|||
response({ |
|||
Contents: {}, |
|||
}) |
|||
) |
|||
} |
|||
|
|||
aws.DynamoDB = {DocumentClient} |
|||
aws.S3 = S3 |
|||
aws.config = {update: jest.fn()} |
|||
|
|||
module.exports = aws |
|||
} |
|||
@ -1,19 +0,0 @@ |
|||
const mongodb = {} |
|||
|
|||
mongodb.MongoClient = function () { |
|||
this.connect = jest.fn() |
|||
this.close = jest.fn() |
|||
this.insertOne = jest.fn() |
|||
this.find = jest.fn(() => ({ toArray: () => [] })) |
|||
|
|||
this.collection = jest.fn(() => ({ |
|||
insertOne: this.insertOne, |
|||
find: this.find, |
|||
})) |
|||
|
|||
this.db = () => ({ |
|||
collection: this.collection, |
|||
}) |
|||
} |
|||
|
|||
module.exports = mongodb |
|||
@ -0,0 +1,21 @@ |
|||
module MongoMock { |
|||
const mongodb: any = {} |
|||
|
|||
mongodb.MongoClient = function () { |
|||
this.connect = jest.fn() |
|||
this.close = jest.fn() |
|||
this.insertOne = jest.fn() |
|||
this.find = jest.fn(() => ({toArray: () => []})) |
|||
|
|||
this.collection = jest.fn(() => ({ |
|||
insertOne: this.insertOne, |
|||
find: this.find, |
|||
})) |
|||
|
|||
this.db = () => ({ |
|||
collection: this.collection, |
|||
}) |
|||
} |
|||
|
|||
module.exports = mongodb |
|||
} |
|||
@ -1,22 +0,0 @@ |
|||
const mssql = {} |
|||
|
|||
mssql.query = jest.fn(() => ({ |
|||
recordset: [ |
|||
{ |
|||
a: "string", |
|||
b: 1, |
|||
}, |
|||
], |
|||
})) |
|||
|
|||
// mssql.connect = jest.fn(() => ({ recordset: [] }))
|
|||
|
|||
mssql.ConnectionPool = jest.fn(() => ({ |
|||
connect: jest.fn(() => ({ |
|||
request: jest.fn(() => ({ |
|||
query: jest.fn(() => ({})), |
|||
})), |
|||
})), |
|||
})) |
|||
|
|||
module.exports = mssql |
|||
@ -0,0 +1,24 @@ |
|||
module MsSqlMock { |
|||
const mssql: any = {} |
|||
|
|||
mssql.query = jest.fn(() => ({ |
|||
recordset: [ |
|||
{ |
|||
a: "string", |
|||
b: 1, |
|||
}, |
|||
], |
|||
})) |
|||
|
|||
// mssql.connect = jest.fn(() => ({ recordset: [] }))
|
|||
|
|||
mssql.ConnectionPool = jest.fn(() => ({ |
|||
connect: jest.fn(() => ({ |
|||
request: jest.fn(() => ({ |
|||
query: jest.fn(() => ({})), |
|||
})), |
|||
})), |
|||
})) |
|||
|
|||
module.exports = mssql |
|||
} |
|||
@ -1,12 +0,0 @@ |
|||
const mysql = {} |
|||
|
|||
const client = { |
|||
connect: jest.fn(), |
|||
query: jest.fn((query, bindings, fn) => { |
|||
fn(null, []) |
|||
}), |
|||
} |
|||
|
|||
mysql.createConnection = jest.fn(() => client) |
|||
|
|||
module.exports = mysql |
|||
@ -0,0 +1,14 @@ |
|||
module MySQLMock { |
|||
const mysql: any = {} |
|||
|
|||
const client = { |
|||
connect: jest.fn(), |
|||
query: jest.fn((query, bindings, fn) => { |
|||
fn(null, []) |
|||
}), |
|||
} |
|||
|
|||
mysql.createConnection = jest.fn(() => client) |
|||
|
|||
module.exports = mysql |
|||
} |
|||
@ -1,58 +0,0 @@ |
|||
const fetch = jest.requireActual("node-fetch") |
|||
|
|||
module.exports = async (url, opts) => { |
|||
function json(body, status = 200) { |
|||
return { |
|||
status, |
|||
headers: { |
|||
get: () => { |
|||
return ["application/json"] |
|||
}, |
|||
}, |
|||
json: async () => { |
|||
return body |
|||
}, |
|||
} |
|||
} |
|||
|
|||
if (url.includes("/api/admin")) { |
|||
return json({ |
|||
email: "test@test.com", |
|||
_id: "us_test@test.com", |
|||
status: "active", |
|||
}) |
|||
} |
|||
// mocked data based on url
|
|||
else if (url.includes("api/apps")) { |
|||
return json({ |
|||
app1: { |
|||
url: "/app1", |
|||
}, |
|||
}) |
|||
} else if (url.includes("test.com")) { |
|||
return json({ |
|||
body: opts.body, |
|||
url, |
|||
method: opts.method, |
|||
}) |
|||
} else if (url.includes("invalid.com")) { |
|||
return json( |
|||
{ |
|||
invalid: true, |
|||
}, |
|||
404 |
|||
) |
|||
} else if (url.includes("_search")) { |
|||
return json({ |
|||
rows: [ |
|||
{ |
|||
doc: { |
|||
_id: "test", |
|||
}, |
|||
}, |
|||
], |
|||
bookmark: "test", |
|||
}) |
|||
} |
|||
return fetch(url, opts) |
|||
} |
|||
@ -0,0 +1,60 @@ |
|||
module FetchMock { |
|||
const fetch = jest.requireActual("node-fetch") |
|||
|
|||
module.exports = async (url: any, opts: any) => { |
|||
function json(body: any, status = 200) { |
|||
return { |
|||
status, |
|||
headers: { |
|||
get: () => { |
|||
return ["application/json"] |
|||
}, |
|||
}, |
|||
json: async () => { |
|||
return body |
|||
}, |
|||
} |
|||
} |
|||
|
|||
if (url.includes("/api/admin")) { |
|||
return json({ |
|||
email: "test@test.com", |
|||
_id: "us_test@test.com", |
|||
status: "active", |
|||
}) |
|||
} |
|||
// mocked data based on url
|
|||
else if (url.includes("api/apps")) { |
|||
return json({ |
|||
app1: { |
|||
url: "/app1", |
|||
}, |
|||
}) |
|||
} else if (url.includes("test.com")) { |
|||
return json({ |
|||
body: opts.body, |
|||
url, |
|||
method: opts.method, |
|||
}) |
|||
} else if (url.includes("invalid.com")) { |
|||
return json( |
|||
{ |
|||
invalid: true, |
|||
}, |
|||
404 |
|||
) |
|||
} else if (url.includes("_search")) { |
|||
return json({ |
|||
rows: [ |
|||
{ |
|||
doc: { |
|||
_id: "test", |
|||
}, |
|||
}, |
|||
], |
|||
bookmark: "test", |
|||
}) |
|||
} |
|||
return fetch(url, opts) |
|||
} |
|||
} |
|||
@ -1,29 +0,0 @@ |
|||
const pg = {} |
|||
|
|||
const query = jest.fn(() => ({ |
|||
rows: [ |
|||
{ |
|||
a: "string", |
|||
b: 1, |
|||
}, |
|||
], |
|||
})) |
|||
|
|||
// constructor
|
|||
function Client() {} |
|||
|
|||
Client.prototype.query = query |
|||
Client.prototype.connect = jest.fn() |
|||
Client.prototype.release = jest.fn() |
|||
|
|||
function Pool() {} |
|||
Pool.prototype.query = query |
|||
Pool.prototype.connect = jest.fn(() => { |
|||
return new Client() |
|||
}) |
|||
|
|||
pg.Client = Client |
|||
pg.Pool = Pool |
|||
pg.queryMock = query |
|||
|
|||
module.exports = pg |
|||
@ -0,0 +1,35 @@ |
|||
module PgMock { |
|||
const pg: any = {} |
|||
|
|||
const query = jest.fn(() => ({ |
|||
rows: [ |
|||
{ |
|||
a: "string", |
|||
b: 1, |
|||
}, |
|||
], |
|||
})) |
|||
|
|||
// constructor
|
|||
function Client() { |
|||
} |
|||
|
|||
Client.prototype.query = query |
|||
Client.prototype.connect = jest.fn() |
|||
Client.prototype.release = jest.fn() |
|||
|
|||
function Pool() { |
|||
} |
|||
|
|||
Pool.prototype.query = query |
|||
Pool.prototype.connect = jest.fn(() => { |
|||
// @ts-ignore
|
|||
return new Client() |
|||
}) |
|||
|
|||
pg.Client = Client |
|||
pg.Pool = Pool |
|||
pg.queryMock = query |
|||
|
|||
module.exports = pg |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
export interface Table { |
|||
_id: string |
|||
_rev?: string |
|||
type?: string |
|||
views?: {} |
|||
name?: string |
|||
primary?: string[] |
|||
schema: { |
|||
[key: string]: { |
|||
// TODO: replace with field types enum when done
|
|||
type: string |
|||
fieldName?: string |
|||
name: string |
|||
constraints?: { |
|||
type?: string |
|||
email?: boolean |
|||
inclusion?: string[] |
|||
length?: { |
|||
minimum?: string | number |
|||
maximum?: string | number |
|||
} |
|||
presence?: boolean |
|||
} |
|||
} |
|||
} |
|||
primaryDisplay?: string |
|||
sourceId?: string |
|||
} |
|||
@ -1,15 +0,0 @@ |
|||
exports.QUERY_TYPES = { |
|||
SQL: "sql", |
|||
JSON: "json", |
|||
FIELDS: "fields", |
|||
} |
|||
|
|||
exports.FIELD_TYPES = { |
|||
STRING: "string", |
|||
BOOLEAN: "boolean", |
|||
NUMBER: "number", |
|||
PASSWORD: "password", |
|||
LIST: "list", |
|||
OBJECT: "object", |
|||
JSON: "json", |
|||
} |
|||
@ -1,130 +0,0 @@ |
|||
const Airtable = require("airtable") |
|||
const { FIELD_TYPES, QUERY_TYPES } = require("./Integration") |
|||
|
|||
const SCHEMA = { |
|||
docs: "https://airtable.com/api", |
|||
description: |
|||
"Airtable is a spreadsheet-database hybrid, with the features of a database but applied to a spreadsheet.", |
|||
friendlyName: "Airtable", |
|||
datasource: { |
|||
apiKey: { |
|||
type: FIELD_TYPES.PASSWORD, |
|||
default: "enter api key", |
|||
required: true, |
|||
}, |
|||
base: { |
|||
type: FIELD_TYPES.STRING, |
|||
default: "mybase", |
|||
required: true, |
|||
}, |
|||
}, |
|||
query: { |
|||
create: { |
|||
type: QUERY_TYPES.FIELDS, |
|||
customisable: true, |
|||
fields: { |
|||
table: { |
|||
type: FIELD_TYPES.STRING, |
|||
required: true, |
|||
}, |
|||
}, |
|||
}, |
|||
read: { |
|||
type: QUERY_TYPES.FIELDS, |
|||
fields: { |
|||
table: { |
|||
type: FIELD_TYPES.STRING, |
|||
required: true, |
|||
}, |
|||
view: { |
|||
type: FIELD_TYPES.STRING, |
|||
required: true, |
|||
}, |
|||
numRecords: { |
|||
type: FIELD_TYPES.NUMBER, |
|||
default: 10, |
|||
}, |
|||
}, |
|||
}, |
|||
update: { |
|||
type: QUERY_TYPES.FIELDS, |
|||
customisable: true, |
|||
fields: { |
|||
id: { |
|||
type: FIELD_TYPES.STRING, |
|||
required: true, |
|||
}, |
|||
}, |
|||
}, |
|||
delete: { |
|||
type: QUERY_TYPES.JSON, |
|||
}, |
|||
}, |
|||
} |
|||
|
|||
class AirtableIntegration { |
|||
constructor(config) { |
|||
this.config = config |
|||
this.client = new Airtable(config).base(config.base) |
|||
} |
|||
|
|||
async create(query) { |
|||
const { table, json } = query |
|||
|
|||
try { |
|||
const records = await this.client(table).create([ |
|||
{ |
|||
fields: json, |
|||
}, |
|||
]) |
|||
return records |
|||
} catch (err) { |
|||
console.error("Error writing to airtable", err) |
|||
throw err |
|||
} |
|||
} |
|||
|
|||
async read(query) { |
|||
try { |
|||
const records = await this.client(query.table) |
|||
.select({ maxRecords: query.numRecords || 10, view: query.view }) |
|||
.firstPage() |
|||
return records.map(({ fields }) => fields) |
|||
} catch (err) { |
|||
console.error("Error writing to airtable", err) |
|||
return [] |
|||
} |
|||
} |
|||
|
|||
async update(query) { |
|||
const { table, id, json } = query |
|||
|
|||
try { |
|||
const records = await this.client(table).update([ |
|||
{ |
|||
id, |
|||
fields: json, |
|||
}, |
|||
]) |
|||
return records |
|||
} catch (err) { |
|||
console.error("Error writing to airtable", err) |
|||
throw err |
|||
} |
|||
} |
|||
|
|||
async delete(query) { |
|||
try { |
|||
const records = await this.client(query.table).destroy(query.ids) |
|||
return records |
|||
} catch (err) { |
|||
console.error("Error writing to airtable", err) |
|||
throw err |
|||
} |
|||
} |
|||
} |
|||
|
|||
module.exports = { |
|||
schema: SCHEMA, |
|||
integration: AirtableIntegration, |
|||
} |
|||
@ -0,0 +1,143 @@ |
|||
import { |
|||
Integration, |
|||
DatasourceFieldTypes, |
|||
QueryTypes, |
|||
} from "./base/definitions" |
|||
|
|||
module AirtableModule { |
|||
const Airtable = require("airtable") |
|||
|
|||
interface AirtableConfig { |
|||
apiKey: string |
|||
base: string |
|||
} |
|||
|
|||
const SCHEMA: Integration = { |
|||
docs: "https://airtable.com/api", |
|||
description: |
|||
"Airtable is a spreadsheet-database hybrid, with the features of a database but applied to a spreadsheet.", |
|||
friendlyName: "Airtable", |
|||
datasource: { |
|||
apiKey: { |
|||
type: DatasourceFieldTypes.PASSWORD, |
|||
default: "enter api key", |
|||
required: true, |
|||
}, |
|||
base: { |
|||
type: DatasourceFieldTypes.STRING, |
|||
default: "mybase", |
|||
required: true, |
|||
}, |
|||
}, |
|||
query: { |
|||
create: { |
|||
type: QueryTypes.FIELDS, |
|||
customisable: true, |
|||
fields: { |
|||
table: { |
|||
type: DatasourceFieldTypes.STRING, |
|||
required: true, |
|||
}, |
|||
}, |
|||
}, |
|||
read: { |
|||
type: QueryTypes.FIELDS, |
|||
fields: { |
|||
table: { |
|||
type: DatasourceFieldTypes.STRING, |
|||
required: true, |
|||
}, |
|||
view: { |
|||
type: DatasourceFieldTypes.STRING, |
|||
required: true, |
|||
}, |
|||
numRecords: { |
|||
type: DatasourceFieldTypes.NUMBER, |
|||
default: 10, |
|||
}, |
|||
}, |
|||
}, |
|||
update: { |
|||
type: QueryTypes.FIELDS, |
|||
customisable: true, |
|||
fields: { |
|||
id: { |
|||
type: DatasourceFieldTypes.STRING, |
|||
required: true, |
|||
}, |
|||
}, |
|||
}, |
|||
delete: { |
|||
type: QueryTypes.JSON, |
|||
}, |
|||
}, |
|||
} |
|||
|
|||
class AirtableIntegration { |
|||
private config: AirtableConfig |
|||
private client: any |
|||
|
|||
constructor(config: AirtableConfig) { |
|||
this.config = config |
|||
this.client = new Airtable(config).base(config.base) |
|||
} |
|||
|
|||
async create(query: { table: any; json: any }) { |
|||
const { table, json } = query |
|||
|
|||
try { |
|||
return await this.client(table).create([ |
|||
{ |
|||
fields: json, |
|||
}, |
|||
]) |
|||
} catch (err) { |
|||
console.error("Error writing to airtable", err) |
|||
throw err |
|||
} |
|||
} |
|||
|
|||
async read(query: { table: any; numRecords: any; view: any }) { |
|||
try { |
|||
const records = await this.client(query.table) |
|||
.select({ maxRecords: query.numRecords || 10, view: query.view }) |
|||
.firstPage() |
|||
// @ts-ignore
|
|||
return records.map(({ fields }) => fields) |
|||
} catch (err) { |
|||
console.error("Error writing to airtable", err) |
|||
return [] |
|||
} |
|||
} |
|||
|
|||
async update(query: { table: any; id: any; json: any }) { |
|||
const { table, id, json } = query |
|||
|
|||
try { |
|||
return await this.client(table).update([ |
|||
{ |
|||
id, |
|||
fields: json, |
|||
}, |
|||
]) |
|||
} catch (err) { |
|||
console.error("Error writing to airtable", err) |
|||
throw err |
|||
} |
|||
} |
|||
|
|||
async delete(query: { table: any; ids: any }) { |
|||
try { |
|||
return await this.client(query.table).destroy(query.ids) |
|||
} catch (err) { |
|||
console.error("Error writing to airtable", err) |
|||
throw err |
|||
} |
|||
} |
|||
} |
|||
|
|||
module.exports = { |
|||
schema: SCHEMA, |
|||
integration: AirtableIntegration, |
|||
} |
|||
} |
|||
@ -1,86 +0,0 @@ |
|||
const { Database, aql } = require("arangojs") |
|||
const { FIELD_TYPES, QUERY_TYPES } = require("./Integration") |
|||
|
|||
const SCHEMA = { |
|||
docs: "https://github.com/arangodb/arangojs", |
|||
friendlyName: "ArangoDB", |
|||
description: |
|||
"ArangoDB is a scalable open-source multi-model database natively supporting graph, document and search. All supported data models & access patterns can be combined in queries allowing for maximal flexibility. ", |
|||
datasource: { |
|||
url: { |
|||
type: FIELD_TYPES.STRING, |
|||
default: "http://localhost:8529", |
|||
required: true, |
|||
}, |
|||
username: { |
|||
type: FIELD_TYPES.STRING, |
|||
default: "root", |
|||
required: true, |
|||
}, |
|||
password: { |
|||
type: FIELD_TYPES.PASSWORD, |
|||
required: true, |
|||
}, |
|||
databaseName: { |
|||
type: FIELD_TYPES.STRING, |
|||
default: "_system", |
|||
required: true, |
|||
}, |
|||
collection: { |
|||
type: FIELD_TYPES.STRING, |
|||
required: true, |
|||
}, |
|||
}, |
|||
query: { |
|||
read: { |
|||
type: QUERY_TYPES.SQL, |
|||
}, |
|||
create: { |
|||
type: QUERY_TYPES.JSON, |
|||
}, |
|||
}, |
|||
} |
|||
|
|||
class ArangoDBIntegration { |
|||
constructor(config) { |
|||
config.auth = { |
|||
username: config.username, |
|||
password: config.password, |
|||
} |
|||
|
|||
this.config = config |
|||
this.client = new Database(config) |
|||
} |
|||
|
|||
async read(query) { |
|||
try { |
|||
const result = await this.client.query(query.sql) |
|||
return result.all() |
|||
} catch (err) { |
|||
console.error("Error querying arangodb", err.message) |
|||
throw err |
|||
} finally { |
|||
this.client.close() |
|||
} |
|||
} |
|||
|
|||
async create(query) { |
|||
const clc = this.client.collection(this.config.collection) |
|||
try { |
|||
const result = await this.client.query( |
|||
aql`INSERT ${query.json} INTO ${clc} RETURN NEW` |
|||
) |
|||
return result.all() |
|||
} catch (err) { |
|||
console.error("Error querying arangodb", err.message) |
|||
throw err |
|||
} finally { |
|||
this.client.close() |
|||
} |
|||
} |
|||
} |
|||
|
|||
module.exports = { |
|||
schema: SCHEMA, |
|||
integration: ArangoDBIntegration, |
|||
} |
|||
@ -0,0 +1,106 @@ |
|||
import { |
|||
Integration, |
|||
DatasourceFieldTypes, |
|||
QueryTypes, |
|||
} from "./base/definitions" |
|||
|
|||
module ArangoModule { |
|||
const { Database, aql } = require("arangojs") |
|||
|
|||
interface ArangodbConfig { |
|||
url: string |
|||
username: string |
|||
password: string |
|||
databaseName: string |
|||
collection: string |
|||
} |
|||
|
|||
const SCHEMA: Integration = { |
|||
docs: "https://github.com/arangodb/arangojs", |
|||
friendlyName: "ArangoDB", |
|||
description: |
|||
"ArangoDB is a scalable open-source multi-model database natively supporting graph, document and search. All supported data models & access patterns can be combined in queries allowing for maximal flexibility. ", |
|||
datasource: { |
|||
url: { |
|||
type: DatasourceFieldTypes.STRING, |
|||
default: "http://localhost:8529", |
|||
required: true, |
|||
}, |
|||
username: { |
|||
type: DatasourceFieldTypes.STRING, |
|||
default: "root", |
|||
required: true, |
|||
}, |
|||
password: { |
|||
type: DatasourceFieldTypes.PASSWORD, |
|||
required: true, |
|||
}, |
|||
databaseName: { |
|||
type: DatasourceFieldTypes.STRING, |
|||
default: "_system", |
|||
required: true, |
|||
}, |
|||
collection: { |
|||
type: DatasourceFieldTypes.STRING, |
|||
required: true, |
|||
}, |
|||
}, |
|||
query: { |
|||
read: { |
|||
type: QueryTypes.SQL, |
|||
}, |
|||
create: { |
|||
type: QueryTypes.JSON, |
|||
}, |
|||
}, |
|||
} |
|||
|
|||
class ArangoDBIntegration { |
|||
private config: ArangodbConfig |
|||
private client: any |
|||
|
|||
constructor(config: ArangodbConfig) { |
|||
const newConfig = { |
|||
auth: { |
|||
username: config.username, |
|||
password: config.password, |
|||
}, |
|||
} |
|||
|
|||
this.config = config |
|||
this.client = new Database(newConfig) |
|||
} |
|||
|
|||
async read(query: { sql: any }) { |
|||
try { |
|||
const result = await this.client.query(query.sql) |
|||
return result.all() |
|||
} catch (err) { |
|||
console.error("Error querying arangodb", err.message) |
|||
throw err |
|||
} finally { |
|||
this.client.close() |
|||
} |
|||
} |
|||
|
|||
async create(query: { json: any }) { |
|||
const clc = this.client.collection(this.config.collection) |
|||
try { |
|||
const result = await this.client.query( |
|||
aql`INSERT ${query.json} INTO ${clc} RETURN NEW` |
|||
) |
|||
return result.all() |
|||
} catch (err) { |
|||
console.error("Error querying arangodb", err.message) |
|||
throw err |
|||
} finally { |
|||
this.client.close() |
|||
} |
|||
} |
|||
} |
|||
|
|||
module.exports = { |
|||
schema: SCHEMA, |
|||
integration: ArangoDBIntegration, |
|||
} |
|||
} |
|||
@ -1,11 +0,0 @@ |
|||
exports.Operation = { |
|||
CREATE: "CREATE", |
|||
READ: "READ", |
|||
UPDATE: "UPDATE", |
|||
DELETE: "DELETE", |
|||
} |
|||
|
|||
exports.SortDirection = { |
|||
ASCENDING: "ASCENDING", |
|||
DESCENDING: "DESCENDING", |
|||
} |
|||
@ -0,0 +1,109 @@ |
|||
export enum Operation { |
|||
CREATE = "CREATE", |
|||
READ = "READ", |
|||
UPDATE = "UPDATE", |
|||
DELETE = "DELETE", |
|||
} |
|||
|
|||
export enum SortDirection { |
|||
ASCENDING = "ASCENDING", |
|||
DESCENDING = "DESCENDING", |
|||
} |
|||
|
|||
export enum QueryTypes { |
|||
SQL = "sql", |
|||
JSON = "json", |
|||
FIELDS = "fields", |
|||
} |
|||
|
|||
export enum DatasourceFieldTypes { |
|||
STRING = "string", |
|||
BOOLEAN = "boolean", |
|||
NUMBER = "number", |
|||
PASSWORD = "password", |
|||
LIST = "list", |
|||
OBJECT = "object", |
|||
JSON = "json", |
|||
} |
|||
|
|||
export interface QueryDefinition { |
|||
type: QueryTypes |
|||
displayName?: string |
|||
readable?: boolean |
|||
customisable?: boolean |
|||
fields?: object |
|||
urlDisplay?: boolean |
|||
} |
|||
|
|||
export interface Integration { |
|||
docs: string |
|||
plus?: boolean |
|||
description: string |
|||
friendlyName: string |
|||
datasource: {} |
|||
query: { |
|||
[key: string]: QueryDefinition |
|||
} |
|||
} |
|||
|
|||
export interface SearchFilters { |
|||
allOr: boolean |
|||
string?: { |
|||
[key: string]: string |
|||
} |
|||
fuzzy?: { |
|||
[key: string]: string |
|||
} |
|||
range?: { |
|||
[key: string]: { |
|||
high: number | string |
|||
low: number | string |
|||
} |
|||
} |
|||
equal?: { |
|||
[key: string]: any |
|||
} |
|||
notEqual?: { |
|||
[key: string]: any |
|||
} |
|||
empty?: { |
|||
[key: string]: any |
|||
} |
|||
notEmpty?: { |
|||
[key: string]: any |
|||
} |
|||
} |
|||
|
|||
export interface QueryJson { |
|||
endpoint: { |
|||
datasourceId: string |
|||
entityId: string |
|||
operation: Operation |
|||
} |
|||
resource: { |
|||
fields: string[] |
|||
} |
|||
filters?: SearchFilters |
|||
sort?: { |
|||
[key: string]: SortDirection |
|||
} |
|||
paginate?: { |
|||
limit: number |
|||
page: string | number |
|||
} |
|||
body?: object |
|||
extra: { |
|||
idFilter?: SearchFilters |
|||
} |
|||
} |
|||
|
|||
export interface SqlQuery { |
|||
sql: string |
|||
bindings?: { |
|||
[key: string]: any |
|||
} |
|||
} |
|||
|
|||
export interface QueryOptions { |
|||
disableReturning?: boolean |
|||
} |
|||
@ -1,95 +0,0 @@ |
|||
const PouchDB = require("pouchdb") |
|||
const { FIELD_TYPES, QUERY_TYPES } = require("./Integration") |
|||
|
|||
const SCHEMA = { |
|||
docs: "https://docs.couchdb.org/en/stable/", |
|||
friendlyName: "CouchDB", |
|||
description: |
|||
"Apache CouchDB is an open-source document-oriented NoSQL database, implemented in Erlang.", |
|||
datasource: { |
|||
url: { |
|||
type: FIELD_TYPES.STRING, |
|||
required: true, |
|||
default: "http://localhost:5984", |
|||
}, |
|||
database: { |
|||
type: FIELD_TYPES.STRING, |
|||
required: true, |
|||
}, |
|||
}, |
|||
query: { |
|||
create: { |
|||
type: QUERY_TYPES.JSON, |
|||
}, |
|||
read: { |
|||
type: QUERY_TYPES.JSON, |
|||
}, |
|||
update: { |
|||
type: QUERY_TYPES.JSON, |
|||
}, |
|||
delete: { |
|||
type: QUERY_TYPES.FIELDS, |
|||
fields: { |
|||
id: { |
|||
type: FIELD_TYPES.STRING, |
|||
required: true, |
|||
}, |
|||
}, |
|||
}, |
|||
}, |
|||
} |
|||
|
|||
class CouchDBIntegration { |
|||
constructor(config) { |
|||
this.config = config |
|||
this.client = new PouchDB(`${config.url}/${config.database}`) |
|||
} |
|||
|
|||
async create(query) { |
|||
try { |
|||
const result = await this.client.post(query.json) |
|||
return result |
|||
} catch (err) { |
|||
console.error("Error writing to couchDB", err) |
|||
throw err |
|||
} |
|||
} |
|||
|
|||
async read(query) { |
|||
try { |
|||
const result = await this.client.allDocs({ |
|||
include_docs: true, |
|||
...query.json, |
|||
}) |
|||
return result.rows.map(row => row.doc) |
|||
} catch (err) { |
|||
console.error("Error querying couchDB", err) |
|||
throw err |
|||
} |
|||
} |
|||
|
|||
async update(query) { |
|||
try { |
|||
const result = await this.client.put(query.json) |
|||
return result |
|||
} catch (err) { |
|||
console.error("Error updating couchDB document", err) |
|||
throw err |
|||
} |
|||
} |
|||
|
|||
async delete(query) { |
|||
try { |
|||
const result = await this.client.remove(query.id) |
|||
return result |
|||
} catch (err) { |
|||
console.error("Error deleting couchDB document", err) |
|||
throw err |
|||
} |
|||
} |
|||
} |
|||
|
|||
module.exports = { |
|||
schema: SCHEMA, |
|||
integration: CouchDBIntegration, |
|||
} |
|||
@ -0,0 +1,107 @@ |
|||
import { |
|||
Integration, |
|||
DatasourceFieldTypes, |
|||
QueryTypes, |
|||
} from "./base/definitions" |
|||
|
|||
module CouchDBModule { |
|||
const PouchDB = require("pouchdb") |
|||
|
|||
interface CouchDBConfig { |
|||
url: string |
|||
database: string |
|||
} |
|||
|
|||
const SCHEMA: Integration = { |
|||
docs: "https://docs.couchdb.org/en/stable/", |
|||
friendlyName: "CouchDB", |
|||
description: |
|||
"Apache CouchDB is an open-source document-oriented NoSQL database, implemented in Erlang.", |
|||
datasource: { |
|||
url: { |
|||
type: DatasourceFieldTypes.STRING, |
|||
required: true, |
|||
default: "http://localhost:5984", |
|||
}, |
|||
database: { |
|||
type: DatasourceFieldTypes.STRING, |
|||
required: true, |
|||
}, |
|||
}, |
|||
query: { |
|||
create: { |
|||
type: QueryTypes.JSON, |
|||
}, |
|||
read: { |
|||
type: QueryTypes.JSON, |
|||
}, |
|||
update: { |
|||
type: QueryTypes.JSON, |
|||
}, |
|||
delete: { |
|||
type: QueryTypes.FIELDS, |
|||
fields: { |
|||
id: { |
|||
type: DatasourceFieldTypes.STRING, |
|||
required: true, |
|||
}, |
|||
}, |
|||
}, |
|||
}, |
|||
} |
|||
|
|||
class CouchDBIntegration { |
|||
private config: CouchDBConfig |
|||
private client: any |
|||
|
|||
constructor(config: CouchDBConfig) { |
|||
this.config = config |
|||
this.client = new PouchDB(`${config.url}/${config.database}`) |
|||
} |
|||
|
|||
async create(query: { json: object }) { |
|||
try { |
|||
return this.client.post(query.json) |
|||
} catch (err) { |
|||
console.error("Error writing to couchDB", err) |
|||
throw err |
|||
} |
|||
} |
|||
|
|||
async read(query: { json: object }) { |
|||
try { |
|||
const result = await this.client.allDocs({ |
|||
include_docs: true, |
|||
...query.json, |
|||
}) |
|||
return result.rows.map((row: { doc: object }) => row.doc) |
|||
} catch (err) { |
|||
console.error("Error querying couchDB", err) |
|||
throw err |
|||
} |
|||
} |
|||
|
|||
async update(query: { json: object }) { |
|||
try { |
|||
return this.client.put(query.json) |
|||
} catch (err) { |
|||
console.error("Error updating couchDB document", err) |
|||
throw err |
|||
} |
|||
} |
|||
|
|||
async delete(query: { id: string }) { |
|||
try { |
|||
return await this.client.remove(query.id) |
|||
} catch (err) { |
|||
console.error("Error deleting couchDB document", err) |
|||
throw err |
|||
} |
|||
} |
|||
} |
|||
|
|||
module.exports = { |
|||
schema: SCHEMA, |
|||
integration: CouchDBIntegration, |
|||
} |
|||
} |
|||
@ -1,200 +0,0 @@ |
|||
const AWS = require("aws-sdk") |
|||
const { FIELD_TYPES, QUERY_TYPES } = require("./Integration") |
|||
const { AWS_REGION } = require("../db/dynamoClient") |
|||
|
|||
const SCHEMA = { |
|||
docs: "https://github.com/dabit3/dynamodb-documentclient-cheat-sheet", |
|||
description: |
|||
"Amazon DynamoDB is a key-value and document database that delivers single-digit millisecond performance at any scale.", |
|||
friendlyName: "DynamoDB", |
|||
datasource: { |
|||
region: { |
|||
type: FIELD_TYPES.STRING, |
|||
required: true, |
|||
default: "us-east-1", |
|||
}, |
|||
accessKeyId: { |
|||
type: FIELD_TYPES.PASSWORD, |
|||
required: true, |
|||
}, |
|||
secretAccessKey: { |
|||
type: FIELD_TYPES.PASSWORD, |
|||
required: true, |
|||
}, |
|||
endpoint: { |
|||
type: FIELD_TYPES.STRING, |
|||
required: false, |
|||
default: "https://dynamodb.us-east-1.amazonaws.com", |
|||
}, |
|||
}, |
|||
query: { |
|||
create: { |
|||
type: QUERY_TYPES.FIELDS, |
|||
customisable: true, |
|||
fields: { |
|||
table: { |
|||
type: FIELD_TYPES.STRING, |
|||
required: true, |
|||
}, |
|||
}, |
|||
}, |
|||
read: { |
|||
type: QUERY_TYPES.FIELDS, |
|||
customisable: true, |
|||
readable: true, |
|||
fields: { |
|||
table: { |
|||
type: FIELD_TYPES.STRING, |
|||
required: true, |
|||
}, |
|||
index: { |
|||
type: FIELD_TYPES.STRING, |
|||
}, |
|||
}, |
|||
}, |
|||
scan: { |
|||
type: QUERY_TYPES.FIELDS, |
|||
customisable: true, |
|||
readable: true, |
|||
fields: { |
|||
table: { |
|||
type: FIELD_TYPES.STRING, |
|||
required: true, |
|||
}, |
|||
index: { |
|||
type: FIELD_TYPES.STRING, |
|||
}, |
|||
}, |
|||
}, |
|||
get: { |
|||
type: QUERY_TYPES.FIELDS, |
|||
customisable: true, |
|||
readable: true, |
|||
fields: { |
|||
table: { |
|||
type: FIELD_TYPES.STRING, |
|||
required: true, |
|||
}, |
|||
}, |
|||
}, |
|||
update: { |
|||
type: QUERY_TYPES.FIELDS, |
|||
customisable: true, |
|||
fields: { |
|||
table: { |
|||
type: FIELD_TYPES.STRING, |
|||
required: true, |
|||
}, |
|||
}, |
|||
}, |
|||
delete: { |
|||
type: QUERY_TYPES.FIELDS, |
|||
customisable: true, |
|||
fields: { |
|||
table: { |
|||
type: FIELD_TYPES.STRING, |
|||
required: true, |
|||
}, |
|||
}, |
|||
}, |
|||
}, |
|||
} |
|||
|
|||
class DynamoDBIntegration { |
|||
constructor(config) { |
|||
this.config = config |
|||
this.connect() |
|||
let options = { |
|||
correctClockSkew: true, |
|||
} |
|||
if (config.endpoint) { |
|||
options.endpoint = config.endpoint |
|||
} |
|||
this.client = new AWS.DynamoDB.DocumentClient({ |
|||
correctClockSkew: true, |
|||
}) |
|||
} |
|||
|
|||
end() { |
|||
this.disconnect() |
|||
} |
|||
|
|||
connect() { |
|||
AWS.config.update(this.config) |
|||
} |
|||
|
|||
disconnect() { |
|||
AWS.config.update({ |
|||
secretAccessKey: undefined, |
|||
accessKeyId: undefined, |
|||
region: AWS_REGION, |
|||
}) |
|||
} |
|||
|
|||
async create(query) { |
|||
const params = { |
|||
TableName: query.table, |
|||
...query.json, |
|||
} |
|||
return this.client.put(params).promise() |
|||
} |
|||
|
|||
async read(query) { |
|||
const params = { |
|||
TableName: query.table, |
|||
...query.json, |
|||
} |
|||
if (query.index) { |
|||
params.IndexName = query.index |
|||
} |
|||
const response = await this.client.query(params).promise() |
|||
if (response.Items) { |
|||
return response.Items |
|||
} |
|||
return response |
|||
} |
|||
|
|||
async scan(query) { |
|||
const params = { |
|||
TableName: query.table, |
|||
...query.json, |
|||
} |
|||
if (query.index) { |
|||
params.IndexName = query.index |
|||
} |
|||
const response = await this.client.scan(params).promise() |
|||
if (response.Items) { |
|||
return response.Items |
|||
} |
|||
return response |
|||
} |
|||
|
|||
async get(query) { |
|||
const params = { |
|||
TableName: query.table, |
|||
...query.json, |
|||
} |
|||
return this.client.get(params).promise() |
|||
} |
|||
|
|||
async update(query) { |
|||
const params = { |
|||
TableName: query.table, |
|||
...query.json, |
|||
} |
|||
return this.client.update(params).promise() |
|||
} |
|||
|
|||
async delete(query) { |
|||
const params = { |
|||
TableName: query.table, |
|||
...query.json, |
|||
} |
|||
return this.client.delete(params).promise() |
|||
} |
|||
} |
|||
|
|||
module.exports = { |
|||
schema: SCHEMA, |
|||
integration: DynamoDBIntegration, |
|||
} |
|||
@ -0,0 +1,211 @@ |
|||
import { |
|||
Integration, |
|||
DatasourceFieldTypes, |
|||
QueryTypes, |
|||
} from "./base/definitions" |
|||
|
|||
module DynamoModule { |
|||
const AWS = require("aws-sdk") |
|||
const { AWS_REGION } = require("../db/dynamoClient") |
|||
|
|||
interface DynamoDBConfig { |
|||
region: string |
|||
accessKeyId: string |
|||
secretAccessKey: string |
|||
endpoint: string |
|||
} |
|||
|
|||
const SCHEMA: Integration = { |
|||
docs: "https://github.com/dabit3/dynamodb-documentclient-cheat-sheet", |
|||
description: |
|||
"Amazon DynamoDB is a key-value and document database that delivers single-digit millisecond performance at any scale.", |
|||
friendlyName: "DynamoDB", |
|||
datasource: { |
|||
region: { |
|||
type: DatasourceFieldTypes.STRING, |
|||
required: true, |
|||
default: "us-east-1", |
|||
}, |
|||
accessKeyId: { |
|||
type: DatasourceFieldTypes.PASSWORD, |
|||
required: true, |
|||
}, |
|||
secretAccessKey: { |
|||
type: DatasourceFieldTypes.PASSWORD, |
|||
required: true, |
|||
}, |
|||
endpoint: { |
|||
type: DatasourceFieldTypes.STRING, |
|||
required: false, |
|||
default: "https://dynamodb.us-east-1.amazonaws.com", |
|||
}, |
|||
}, |
|||
query: { |
|||
create: { |
|||
type: QueryTypes.FIELDS, |
|||
customisable: true, |
|||
fields: { |
|||
table: { |
|||
type: DatasourceFieldTypes.STRING, |
|||
required: true, |
|||
}, |
|||
}, |
|||
}, |
|||
read: { |
|||
type: QueryTypes.FIELDS, |
|||
customisable: true, |
|||
readable: true, |
|||
fields: { |
|||
table: { |
|||
type: DatasourceFieldTypes.STRING, |
|||
required: true, |
|||
}, |
|||
index: { |
|||
type: DatasourceFieldTypes.STRING, |
|||
}, |
|||
}, |
|||
}, |
|||
scan: { |
|||
type: QueryTypes.FIELDS, |
|||
customisable: true, |
|||
readable: true, |
|||
fields: { |
|||
table: { |
|||
type: DatasourceFieldTypes.STRING, |
|||
required: true, |
|||
}, |
|||
index: { |
|||
type: DatasourceFieldTypes.STRING, |
|||
}, |
|||
}, |
|||
}, |
|||
get: { |
|||
type: QueryTypes.FIELDS, |
|||
customisable: true, |
|||
readable: true, |
|||
fields: { |
|||
table: { |
|||
type: DatasourceFieldTypes.STRING, |
|||
required: true, |
|||
}, |
|||
}, |
|||
}, |
|||
update: { |
|||
type: QueryTypes.FIELDS, |
|||
customisable: true, |
|||
fields: { |
|||
table: { |
|||
type: DatasourceFieldTypes.STRING, |
|||
required: true, |
|||
}, |
|||
}, |
|||
}, |
|||
delete: { |
|||
type: QueryTypes.FIELDS, |
|||
customisable: true, |
|||
fields: { |
|||
table: { |
|||
type: DatasourceFieldTypes.STRING, |
|||
required: true, |
|||
}, |
|||
}, |
|||
}, |
|||
}, |
|||
} |
|||
|
|||
class DynamoDBIntegration { |
|||
private config: DynamoDBConfig |
|||
private client: any |
|||
|
|||
constructor(config: DynamoDBConfig) { |
|||
this.config = config |
|||
this.connect() |
|||
let options = { |
|||
correctClockSkew: true, |
|||
endpoint: config.endpoint ? config.endpoint : undefined, |
|||
} |
|||
this.client = new AWS.DynamoDB.DocumentClient(options) |
|||
} |
|||
|
|||
end() { |
|||
this.disconnect() |
|||
} |
|||
|
|||
connect() { |
|||
AWS.config.update(this.config) |
|||
} |
|||
|
|||
disconnect() { |
|||
AWS.config.update({ |
|||
secretAccessKey: undefined, |
|||
accessKeyId: undefined, |
|||
region: AWS_REGION, |
|||
}) |
|||
} |
|||
|
|||
async create(query: { table: string; json: object }) { |
|||
const params = { |
|||
TableName: query.table, |
|||
...query.json, |
|||
} |
|||
return this.client.put(params).promise() |
|||
} |
|||
|
|||
async read(query: { table: string; json: object; index: null | string }) { |
|||
const params = { |
|||
TableName: query.table, |
|||
IndexName: query.index ? query.index : undefined, |
|||
...query.json, |
|||
} |
|||
if (query.index) { |
|||
const response = await this.client.query(params).promise() |
|||
if (response.Items) { |
|||
return response.Items |
|||
} |
|||
return response |
|||
} |
|||
} |
|||
|
|||
async scan(query: { table: string; json: object; index: null | string }) { |
|||
const params = { |
|||
TableName: query.table, |
|||
IndexName: query.index ? query.index : undefined, |
|||
...query.json, |
|||
} |
|||
const response = await this.client.scan(params).promise() |
|||
if (response.Items) { |
|||
return response.Items |
|||
} |
|||
return response |
|||
} |
|||
|
|||
async get(query: { table: string; json: object }) { |
|||
const params = { |
|||
TableName: query.table, |
|||
...query.json, |
|||
} |
|||
return this.client.get(params).promise() |
|||
} |
|||
|
|||
async update(query: { table: string; json: object }) { |
|||
const params = { |
|||
TableName: query.table, |
|||
...query.json, |
|||
} |
|||
return this.client.update(params).promise() |
|||
} |
|||
|
|||
async delete(query: { table: string; json: object }) { |
|||
const params = { |
|||
TableName: query.table, |
|||
...query.json, |
|||
} |
|||
return this.client.delete(params).promise() |
|||
} |
|||
} |
|||
|
|||
module.exports = { |
|||
schema: SCHEMA, |
|||
integration: DynamoDBIntegration, |
|||
} |
|||
} |
|||
@ -1,139 +0,0 @@ |
|||
const { Client } = require("@elastic/elasticsearch") |
|||
const { QUERY_TYPES, FIELD_TYPES } = require("./Integration") |
|||
|
|||
const SCHEMA = { |
|||
docs: "https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/index.html", |
|||
description: |
|||
"Elasticsearch is a search engine based on the Lucene library. It provides a distributed, multitenant-capable full-text search engine with an HTTP web interface and schema-free JSON documents.", |
|||
friendlyName: "ElasticSearch", |
|||
datasource: { |
|||
url: { |
|||
type: "string", |
|||
required: true, |
|||
default: "http://localhost:9200", |
|||
}, |
|||
}, |
|||
query: { |
|||
create: { |
|||
type: QUERY_TYPES.FIELDS, |
|||
customisable: true, |
|||
fields: { |
|||
index: { |
|||
type: FIELD_TYPES.STRING, |
|||
required: true, |
|||
}, |
|||
}, |
|||
}, |
|||
read: { |
|||
type: QUERY_TYPES.FIELDS, |
|||
customisable: true, |
|||
fields: { |
|||
index: { |
|||
type: FIELD_TYPES.STRING, |
|||
required: true, |
|||
}, |
|||
}, |
|||
}, |
|||
update: { |
|||
type: QUERY_TYPES.FIELDS, |
|||
customisable: true, |
|||
fields: { |
|||
id: { |
|||
type: FIELD_TYPES.STRING, |
|||
required: true, |
|||
}, |
|||
index: { |
|||
type: FIELD_TYPES.STRING, |
|||
required: true, |
|||
}, |
|||
}, |
|||
}, |
|||
delete: { |
|||
type: QUERY_TYPES.FIELDS, |
|||
fields: { |
|||
index: { |
|||
type: FIELD_TYPES.STRING, |
|||
required: true, |
|||
}, |
|||
id: { |
|||
type: FIELD_TYPES.STRING, |
|||
required: true, |
|||
}, |
|||
}, |
|||
}, |
|||
}, |
|||
} |
|||
|
|||
class ElasticSearchIntegration { |
|||
constructor(config) { |
|||
this.config = config |
|||
this.client = new Client({ node: config.url }) |
|||
} |
|||
|
|||
async create(query) { |
|||
const { index, json } = query |
|||
|
|||
try { |
|||
const result = await this.client.index({ |
|||
index, |
|||
body: json, |
|||
}) |
|||
return result.body |
|||
} catch (err) { |
|||
console.error("Error writing to elasticsearch", err) |
|||
throw err |
|||
} finally { |
|||
await this.client.close() |
|||
} |
|||
} |
|||
|
|||
async read(query) { |
|||
const { index, json } = query |
|||
try { |
|||
const result = await this.client.search({ |
|||
index: index, |
|||
body: json, |
|||
}) |
|||
return result.body.hits.hits.map(({ _source }) => _source) |
|||
} catch (err) { |
|||
console.error("Error querying elasticsearch", err) |
|||
throw err |
|||
} finally { |
|||
await this.client.close() |
|||
} |
|||
} |
|||
|
|||
async update(query) { |
|||
const { id, index, json } = query |
|||
try { |
|||
const result = await this.client.update({ |
|||
id, |
|||
index, |
|||
body: json, |
|||
}) |
|||
return result.body |
|||
} catch (err) { |
|||
console.error("Error querying elasticsearch", err) |
|||
throw err |
|||
} finally { |
|||
await this.client.close() |
|||
} |
|||
} |
|||
|
|||
async delete(query) { |
|||
try { |
|||
const result = await this.client.delete(query) |
|||
return result.body |
|||
} catch (err) { |
|||
console.error("Error deleting from elasticsearch", err) |
|||
throw err |
|||
} finally { |
|||
await this.client.close() |
|||
} |
|||
} |
|||
} |
|||
|
|||
module.exports = { |
|||
schema: SCHEMA, |
|||
integration: ElasticSearchIntegration, |
|||
} |
|||
@ -0,0 +1,153 @@ |
|||
import { |
|||
Integration, |
|||
DatasourceFieldTypes, |
|||
QueryTypes, |
|||
} from "./base/definitions" |
|||
|
|||
module ElasticsearchModule { |
|||
const { Client } = require("@elastic/elasticsearch") |
|||
|
|||
interface ElasticsearchConfig { |
|||
url: string |
|||
} |
|||
|
|||
const SCHEMA: Integration = { |
|||
docs: "https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/index.html", |
|||
description: |
|||
"Elasticsearch is a search engine based on the Lucene library. It provides a distributed, multitenant-capable full-text search engine with an HTTP web interface and schema-free JSON documents.", |
|||
friendlyName: "ElasticSearch", |
|||
datasource: { |
|||
url: { |
|||
type: DatasourceFieldTypes.STRING, |
|||
required: true, |
|||
default: "http://localhost:9200", |
|||
}, |
|||
}, |
|||
query: { |
|||
create: { |
|||
type: QueryTypes.FIELDS, |
|||
customisable: true, |
|||
fields: { |
|||
index: { |
|||
type: DatasourceFieldTypes.STRING, |
|||
required: true, |
|||
}, |
|||
}, |
|||
}, |
|||
read: { |
|||
type: QueryTypes.FIELDS, |
|||
customisable: true, |
|||
fields: { |
|||
index: { |
|||
type: DatasourceFieldTypes.STRING, |
|||
required: true, |
|||
}, |
|||
}, |
|||
}, |
|||
update: { |
|||
type: QueryTypes.FIELDS, |
|||
customisable: true, |
|||
fields: { |
|||
id: { |
|||
type: DatasourceFieldTypes.STRING, |
|||
required: true, |
|||
}, |
|||
index: { |
|||
type: DatasourceFieldTypes.STRING, |
|||
required: true, |
|||
}, |
|||
}, |
|||
}, |
|||
delete: { |
|||
type: QueryTypes.FIELDS, |
|||
fields: { |
|||
index: { |
|||
type: DatasourceFieldTypes.STRING, |
|||
required: true, |
|||
}, |
|||
id: { |
|||
type: DatasourceFieldTypes.STRING, |
|||
required: true, |
|||
}, |
|||
}, |
|||
}, |
|||
}, |
|||
} |
|||
|
|||
class ElasticSearchIntegration { |
|||
private config: ElasticsearchConfig |
|||
private client: any |
|||
|
|||
constructor(config: ElasticsearchConfig) { |
|||
this.config = config |
|||
this.client = new Client({ node: config.url }) |
|||
} |
|||
|
|||
async create(query: { index: string; json: object }) { |
|||
const { index, json } = query |
|||
|
|||
try { |
|||
const result = await this.client.index({ |
|||
index, |
|||
body: json, |
|||
}) |
|||
return result.body |
|||
} catch (err) { |
|||
console.error("Error writing to elasticsearch", err) |
|||
throw err |
|||
} finally { |
|||
await this.client.close() |
|||
} |
|||
} |
|||
|
|||
async read(query: { index: string; json: object }) { |
|||
const { index, json } = query |
|||
try { |
|||
const result = await this.client.search({ |
|||
index: index, |
|||
body: json, |
|||
}) |
|||
return result.body.hits.hits.map(({ _source }: any) => _source) |
|||
} catch (err) { |
|||
console.error("Error querying elasticsearch", err) |
|||
throw err |
|||
} finally { |
|||
await this.client.close() |
|||
} |
|||
} |
|||
|
|||
async update(query: { id: string; index: string; json: object }) { |
|||
const { id, index, json } = query |
|||
try { |
|||
const result = await this.client.update({ |
|||
id, |
|||
index, |
|||
body: json, |
|||
}) |
|||
return result.body |
|||
} catch (err) { |
|||
console.error("Error querying elasticsearch", err) |
|||
throw err |
|||
} finally { |
|||
await this.client.close() |
|||
} |
|||
} |
|||
|
|||
async delete(query: object) { |
|||
try { |
|||
const result = await this.client.delete(query) |
|||
return result.body |
|||
} catch (err) { |
|||
console.error("Error deleting from elasticsearch", err) |
|||
throw err |
|||
} finally { |
|||
await this.client.close() |
|||
} |
|||
} |
|||
} |
|||
|
|||
module.exports = { |
|||
schema: SCHEMA, |
|||
integration: ElasticSearchIntegration, |
|||
} |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue