mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-21 00:02:23 +00:00
feat(frontend): Added support for mysql, mssql and snowflake in the Database Studio (#3250)
* feat(frontend): Added support for mysql in the Database Studio * feat(frontend): generic db * feat(frontend): generic column def wip * fix(frontend): adapt queries * fix(frontend): adapt queries * fix(frontend): wip * fix(frontend): wip * feat(frontend): remove useless runnable calls * feat(frontend): MySQL + PSQL done, mssql wip * feat(frontend): clean up * feat(frontend): clean up * feat(frontend): mssql: select + count done * feat(frontend): mssql: update, insert and delete + fix policies * feat(frontend): typo * feat(frontend): fix build * feat(frontend): improve perf + UI * feat(frontend): add snowflake * feat(frontend): fix count * feat(frontend): debounce input * feat(frontend): fix deploy button * feat(frontend): remove ghost rows * feat(frontend): Fix search * feat(frontend): Fix search * feat(fontend): remove bigquery * feat(fontend): Fix performance issues + sorting * feat(fontend): Fix lastRow * feat(fontend): Fix build + search * feat(fontend): fix mysql metadata * feat(fontend): simplify code * feat(fontend): remove cache + fix the number of calls * feat(fontend): clean up * feat(fontend): clean up * feat(fontend): remove dead code * feat(fontend): fix infinite scroll * feat(fontend): fix count * feat(fontend): remove unnecessary clearRows * feat(fontend): add missing delete policy * feat(fontend): fix search * feat(fontend): fix search * feat(fontend): roll back infiniteInitialRowCount * feat(fontend): roll back infiniteInitialRowCount * feat(fontend): add bigquery (#3326) * feat(fontend): add bigquery --------- Co-authored-by: HugoCasa <hugo@casademont.ch> * feat(fontend): remove code duplication * feat(fontend): add mapping for postgres --------- Co-authored-by: HugoCasa <hugo@casademont.ch>
This commit is contained in:
@@ -41,6 +41,7 @@
|
||||
import { workspacedOpenai } from './copilot/lib'
|
||||
import type { FlowCopilotContext, FlowCopilotModule } from './copilot/flow'
|
||||
import { pickScript } from './flows/flowStateUtils'
|
||||
import type { Schedule } from './flows/scheduleUtils'
|
||||
|
||||
$: token = $page.url.searchParams.get('wm_token') ?? undefined
|
||||
$: workspace = $page.url.searchParams.get('workspace') ?? undefined
|
||||
@@ -346,11 +347,12 @@
|
||||
}
|
||||
|
||||
const flowStateStore = writable({} as FlowState)
|
||||
const scheduleStore = writable({
|
||||
const scheduleStore = writable<Schedule>({
|
||||
args: {},
|
||||
cron: '',
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
enabled: false
|
||||
enabled: false,
|
||||
summary: undefined
|
||||
})
|
||||
const previewArgsStore = writable<Record<string, any>>({})
|
||||
const scriptEditorDrawer = writable(undefined)
|
||||
|
||||
+239
-128
@@ -3,22 +3,23 @@
|
||||
AppEditorContext,
|
||||
AppViewerContext,
|
||||
ComponentCustomCSS,
|
||||
OneOfConfiguration,
|
||||
RichConfigurations
|
||||
} from '../../../types'
|
||||
import { components } from '$lib/components/apps/editor/component'
|
||||
import ResolveConfig from '../../helpers/ResolveConfig.svelte'
|
||||
import { findGridItem, initConfig, initOutput } from '$lib/components/apps/editor/appUtils'
|
||||
import {
|
||||
createPostgresInput,
|
||||
getDbSchemas,
|
||||
loadTableMetaData,
|
||||
type ColumnMetadata,
|
||||
type TableMetadata,
|
||||
getPrimaryKeys
|
||||
getPrimaryKeys,
|
||||
type ColumnDef,
|
||||
type DbType
|
||||
} from './utils'
|
||||
import { getContext, tick } from 'svelte'
|
||||
import UpdateCell from './UpdateCell.svelte'
|
||||
import { workspaceStore, type DBSchemas } from '$lib/stores'
|
||||
import { workspaceStore, type DBSchemas, type DBSchema } from '$lib/stores'
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import { Plus } from 'lucide-svelte'
|
||||
import { Drawer, DrawerContent } from '$lib/components/common'
|
||||
@@ -34,6 +35,8 @@
|
||||
import InsertRowRunnable from './InsertRowRunnable.svelte'
|
||||
import DeleteRow from './DeleteRow.svelte'
|
||||
import InitializeComponent from '../../helpers/InitializeComponent.svelte'
|
||||
import { getSelectInput } from './queries/select'
|
||||
import DebouncedInput from '../../helpers/DebouncedInput.svelte'
|
||||
|
||||
export let id: string
|
||||
export let configuration: RichConfigurations
|
||||
@@ -46,6 +49,32 @@
|
||||
configuration
|
||||
)
|
||||
|
||||
$: computeInput(
|
||||
resolvedConfig.columnDefs,
|
||||
resolvedConfig.whereClause,
|
||||
resolvedConfig.type.configuration[resolvedConfig.type.selected].resource
|
||||
)
|
||||
|
||||
let timeoutInput: NodeJS.Timeout | undefined = undefined
|
||||
|
||||
function computeInput(columnDefs: any, whereClause: string | undefined, resource: any) {
|
||||
if (timeoutInput) {
|
||||
clearTimeout(timeoutInput)
|
||||
}
|
||||
timeoutInput = setTimeout(() => {
|
||||
timeoutInput = undefined
|
||||
console.log('compute input')
|
||||
|
||||
input = getSelectInput(
|
||||
resource,
|
||||
resolvedConfig.type.configuration[resolvedConfig.type.selected].table,
|
||||
columnDefs,
|
||||
whereClause,
|
||||
resolvedConfig.type.selected as DbType
|
||||
)
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const { app, worldStore, mode, selectedComponent } =
|
||||
getContext<AppViewerContext>('AppViewerContext')
|
||||
const editorContext = getContext<AppEditorContext>('AppEditorContext')
|
||||
@@ -54,39 +83,15 @@
|
||||
let quicksearch = ''
|
||||
let aggrid: AppAggridExplorerTable
|
||||
|
||||
$: computeInput(
|
||||
resolvedConfig.columnDefs,
|
||||
resolvedConfig.whereClause,
|
||||
resolvedConfig.type.configuration.postgresql.resource
|
||||
)
|
||||
|
||||
let timeoutInput: NodeJS.Timeout | undefined = undefined
|
||||
function computeInput(columnDefs: any, whereClause: string | undefined, resource: any) {
|
||||
if (timeoutInput) {
|
||||
clearTimeout(timeoutInput)
|
||||
}
|
||||
timeoutInput = setTimeout(() => {
|
||||
timeoutInput = undefined
|
||||
console.log('compute input')
|
||||
aggrid?.clearRows()
|
||||
input = createPostgresInput(
|
||||
resource,
|
||||
resolvedConfig.type.configuration.postgresql.table,
|
||||
columnDefs,
|
||||
whereClause
|
||||
)
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
$: editorContext != undefined && $mode == 'dnd' && resolvedConfig.type && listTableIfAvailable()
|
||||
|
||||
$: editorContext != undefined &&
|
||||
$mode == 'dnd' &&
|
||||
resolvedConfig.type.configuration?.postgresql?.table &&
|
||||
resolvedConfig.type.configuration?.[resolvedConfig?.type?.selected]?.table &&
|
||||
listColumnsIfAvailable()
|
||||
|
||||
let firstQuicksearch = true
|
||||
$: if (quicksearch) {
|
||||
$: if (quicksearch !== undefined) {
|
||||
if (firstQuicksearch) {
|
||||
firstQuicksearch = false
|
||||
} else {
|
||||
@@ -106,7 +111,7 @@
|
||||
function onUpdate(
|
||||
e: CustomEvent<{
|
||||
row: number
|
||||
columnDef: ColumnMetadata
|
||||
columnDef: ColumnDef
|
||||
column: string
|
||||
value: any
|
||||
data: any
|
||||
@@ -116,13 +121,14 @@
|
||||
const { columnDef, value, data, oldValue } = e.detail
|
||||
|
||||
updateCell?.triggerUpdate(
|
||||
resolvedConfig.type.configuration.postgresql.resource,
|
||||
resolvedConfig.type.configuration.postgresql.table ?? 'unknown',
|
||||
resolvedConfig.type.configuration[resolvedConfig.type.selected].resource,
|
||||
resolvedConfig.type.configuration[resolvedConfig.type.selected].table ?? 'unknown',
|
||||
columnDef,
|
||||
resolvedConfig.columnDefs,
|
||||
value,
|
||||
data,
|
||||
oldValue
|
||||
oldValue,
|
||||
resolvedConfig.type.selected as DbType
|
||||
)
|
||||
}
|
||||
|
||||
@@ -140,8 +146,30 @@
|
||||
})
|
||||
|
||||
let lastResource: string | undefined = undefined
|
||||
|
||||
function updateOneOfConfiguration<T, U extends string, V>(
|
||||
oneOfConfiguration: OneOfConfiguration,
|
||||
resolvedConfig: {
|
||||
configuration: Record<U, V>
|
||||
selected: U
|
||||
},
|
||||
patch: Partial<Record<keyof V, any>>
|
||||
) {
|
||||
const selectedConfig = oneOfConfiguration.configuration[resolvedConfig.selected]
|
||||
if (!selectedConfig) {
|
||||
console.warn(`Selected configuration '${resolvedConfig.selected}' does not exist.`)
|
||||
return
|
||||
}
|
||||
Object.keys(patch).forEach((key) => {
|
||||
oneOfConfiguration.configuration[resolvedConfig.selected][key] = {
|
||||
...oneOfConfiguration.configuration[resolvedConfig.selected][key],
|
||||
...patch[key]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function listTableIfAvailable() {
|
||||
let resource = resolvedConfig.type.configuration?.postgresql?.resource
|
||||
let resource = resolvedConfig.type.configuration?.[resolvedConfig.type.selected]?.resource
|
||||
if (lastResource === resource) return
|
||||
lastResource = resource
|
||||
const gridItem = findGridItem($app, id)
|
||||
@@ -150,105 +178,143 @@
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
'configuration' in gridItem.data?.configuration?.type &&
|
||||
'selectOptions' in gridItem.data?.configuration?.type?.configuration?.postgresql?.table
|
||||
) {
|
||||
gridItem.data.configuration.type.configuration.postgresql.table.selectOptions = []
|
||||
}
|
||||
updateOneOfConfiguration(
|
||||
gridItem.data.configuration.type as OneOfConfiguration,
|
||||
resolvedConfig.type,
|
||||
{
|
||||
table: {
|
||||
selectOptions: [],
|
||||
loading: true
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (!resolvedConfig.type?.configuration?.postgresql?.resource) {
|
||||
if (!resolvedConfig.type?.configuration?.[resolvedConfig.type.selected]?.resource) {
|
||||
$app = {
|
||||
...$app
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
'configuration' in gridItem.data?.configuration?.type &&
|
||||
gridItem.data.configuration.type.configuration.postgresql.table
|
||||
) {
|
||||
gridItem.data.configuration.type.configuration.postgresql.table.loading = true
|
||||
}
|
||||
|
||||
try {
|
||||
const dbSchemas: DBSchemas = {}
|
||||
|
||||
await getDbSchemas(
|
||||
'postgresql',
|
||||
resolvedConfig.type.configuration.postgresql.resource.split(':')[1],
|
||||
resolvedConfig?.type?.selected,
|
||||
resolvedConfig.type.configuration[resolvedConfig?.type?.selected].resource.split(':')[1],
|
||||
$workspaceStore,
|
||||
dbSchemas,
|
||||
(message: string) => {}
|
||||
)
|
||||
|
||||
if ('configuration' in gridItem.data.configuration.type) {
|
||||
gridItem.data.configuration.type.configuration.postgresql.table['selectOptions'] = dbSchemas
|
||||
? // @ts-ignore
|
||||
Object.keys(Object.values(dbSchemas)?.[0]?.schema?.public ?? {})
|
||||
: []
|
||||
}
|
||||
updateOneOfConfiguration(
|
||||
gridItem.data.configuration.type as OneOfConfiguration,
|
||||
resolvedConfig.type,
|
||||
{
|
||||
table: {
|
||||
selectOptions: dbSchemas ? getTablesByResource(dbSchemas) : [],
|
||||
loading: false
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
$app = {
|
||||
...$app
|
||||
}
|
||||
} catch (e) {}
|
||||
if (
|
||||
'configuration' in gridItem.data?.configuration?.type &&
|
||||
gridItem.data.configuration.type.configuration.postgresql.table
|
||||
) {
|
||||
gridItem.data.configuration.type.configuration.postgresql.table.loading = false
|
||||
}
|
||||
|
||||
function getTablesByResource(schema: Partial<Record<string, DBSchema>>) {
|
||||
const s = Object.values(schema)?.[0]
|
||||
switch (resolvedConfig.type.selected) {
|
||||
case 'postgresql':
|
||||
if (s?.lang === 'postgresql') {
|
||||
return Object.keys(s.schema?.public ?? s.schema ?? {})
|
||||
}
|
||||
case 'mysql':
|
||||
return Object.keys(Object.values(s?.schema ?? {})?.[0])
|
||||
case 'ms_sql_server':
|
||||
return Object.keys(Object.values(s?.schema ?? {})?.[0])
|
||||
case 'snowflake': {
|
||||
return Object.keys(Object.values(s?.schema ?? {})?.[0])
|
||||
}
|
||||
|
||||
case 'bigquery': {
|
||||
const paths: string[] = []
|
||||
for (const key in s?.schema) {
|
||||
if (s?.schema.hasOwnProperty(key)) {
|
||||
const subObj = s?.schema[key]
|
||||
for (const subKey in subObj) {
|
||||
if (subObj.hasOwnProperty(subKey)) {
|
||||
paths.push(`${key}.${subKey}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return paths
|
||||
}
|
||||
|
||||
default:
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
let datasource: IDatasource = {
|
||||
rowCount: 0,
|
||||
getRows: async function (params) {
|
||||
let uuid = await runnableComponent?.runComponent(
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
{
|
||||
offset: params.startRow,
|
||||
limit: params.endRow - params.startRow,
|
||||
quicksearch,
|
||||
orderBy: params.sortModel?.[0]?.colId ?? resolvedConfig.columnDefs?.[0]?.field,
|
||||
is_desc: params.sortModel?.[0]?.sort === 'desc'
|
||||
},
|
||||
{
|
||||
done: (x) => {
|
||||
let lastRow = -1
|
||||
if (datasource.rowCount && datasource.rowCount <= params.endRow) {
|
||||
lastRow = datasource.rowCount
|
||||
const currentParams = {
|
||||
offset: params.startRow,
|
||||
limit: params.endRow - params.startRow,
|
||||
quicksearch,
|
||||
order_by: params.sortModel?.[0]?.colId ?? resolvedConfig.columnDefs?.[0]?.field,
|
||||
is_desc: params.sortModel?.[0]?.sort === 'desc'
|
||||
}
|
||||
|
||||
if (!render) {
|
||||
return
|
||||
}
|
||||
|
||||
runnableComponent?.runComponent(undefined, undefined, undefined, currentParams, {
|
||||
done: (items) => {
|
||||
let lastRow = -1
|
||||
|
||||
if (datasource?.rowCount && datasource.rowCount <= params.endRow) {
|
||||
lastRow = datasource.rowCount
|
||||
}
|
||||
|
||||
if (items && Array.isArray(items)) {
|
||||
// MsSql response have an outer array, we need to flatten it
|
||||
if (resolvedConfig.type.selected === 'ms_sql_server') {
|
||||
items = items?.[0]
|
||||
}
|
||||
if (x && Array.isArray(x)) {
|
||||
params.successCallback(
|
||||
x.map((x) => {
|
||||
let primaryKeys = getPrimaryKeys(resolvedConfig.columnDefs)
|
||||
let o = {}
|
||||
primaryKeys.forEach((pk) => {
|
||||
o[pk] = x[pk]
|
||||
})
|
||||
x['__index'] = JSON.stringify(o)
|
||||
return x
|
||||
}),
|
||||
lastRow
|
||||
)
|
||||
} else {
|
||||
params.failCallback()
|
||||
|
||||
let processedData = items.map((item) => {
|
||||
let primaryKeys = getPrimaryKeys(resolvedConfig.columnDefs)
|
||||
let o = {}
|
||||
primaryKeys.forEach((pk) => {
|
||||
o[pk] = item[pk]
|
||||
})
|
||||
item['__index'] = JSON.stringify(o)
|
||||
return item
|
||||
})
|
||||
|
||||
if (items.length < params.endRow - params.startRow) {
|
||||
lastRow = params.startRow + items.length
|
||||
}
|
||||
},
|
||||
cancel: () => {
|
||||
console.log('cancel datasource request')
|
||||
params.failCallback()
|
||||
},
|
||||
error: () => {
|
||||
console.log('error datasource request')
|
||||
|
||||
params.successCallback(processedData, lastRow)
|
||||
} else {
|
||||
params.failCallback()
|
||||
}
|
||||
},
|
||||
cancel: () => {
|
||||
params.failCallback()
|
||||
},
|
||||
error: () => {
|
||||
params.failCallback()
|
||||
}
|
||||
)
|
||||
console.log('asking for ' + params.startRow + ' to ' + params.endRow, uuid)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -256,15 +322,19 @@
|
||||
|
||||
let timeout: NodeJS.Timeout | undefined = undefined
|
||||
async function listColumnsIfAvailable() {
|
||||
let table = resolvedConfig.type.configuration?.postgresql?.table
|
||||
if (lastTable === table) return
|
||||
lastTable = table
|
||||
const selected = resolvedConfig.type.selected
|
||||
let table = resolvedConfig.type.configuration?.[resolvedConfig.type.selected]?.table
|
||||
|
||||
if (lastTable === table) return
|
||||
|
||||
lastTable = table
|
||||
let tableMetadata = await loadTableMetaData(
|
||||
resolvedConfig.type.configuration.postgresql.resource,
|
||||
resolvedConfig.type.configuration[selected].resource,
|
||||
$workspaceStore,
|
||||
resolvedConfig.type.configuration.postgresql.table
|
||||
resolvedConfig.type.configuration[selected].table,
|
||||
selected
|
||||
)
|
||||
|
||||
if (!tableMetadata) return
|
||||
|
||||
const gridItem = findGridItem($app, id)
|
||||
@@ -281,6 +351,11 @@
|
||||
const oldMap = Object.fromEntries(old.filter((x) => x != undefined).map((x) => [x.field, x]))
|
||||
const newMap = Object.fromEntries(tableMetadata?.map((x) => [x.field, x]) ?? [])
|
||||
|
||||
// if they are the same, do nothing
|
||||
if (JSON.stringify(oldMap) === JSON.stringify(newMap)) {
|
||||
return
|
||||
}
|
||||
|
||||
let ncols: any[] = []
|
||||
Object.entries(oldMap).forEach(([key, value]) => {
|
||||
if (newMap[key]) {
|
||||
@@ -296,6 +371,15 @@
|
||||
}
|
||||
})
|
||||
|
||||
// Mysql capitalizes the column names, so we make sure to lowercase them
|
||||
ncols = ncols.map((x) => {
|
||||
let o = {}
|
||||
Object.keys(x).forEach((k) => {
|
||||
o[k.toLowerCase()] = x[k]
|
||||
})
|
||||
return o
|
||||
})
|
||||
|
||||
state = undefined
|
||||
|
||||
//@ts-ignore
|
||||
@@ -321,14 +405,29 @@
|
||||
$: $worldStore && connectToComponents()
|
||||
|
||||
function connectToComponents() {
|
||||
if ($worldStore) {
|
||||
if ($worldStore && datasource !== undefined) {
|
||||
const outputs = $worldStore.outputsById[`${id}_count`]
|
||||
|
||||
if (outputs) {
|
||||
outputs.result.subscribe(
|
||||
{
|
||||
id: 'dbexplorer-count-' + id,
|
||||
next: (value) => {
|
||||
datasource.rowCount = value?.[0]?.count
|
||||
// MsSql response have an outer array, we need to flatten it
|
||||
if (
|
||||
Array.isArray(value) &&
|
||||
value.length === 1 &&
|
||||
resolvedConfig.type.selected === 'ms_sql_server'
|
||||
) {
|
||||
// @ts-ignore
|
||||
datasource.rowCount = value?.[0]?.[0]?.count
|
||||
} else if (resolvedConfig.type.selected === 'snowflake') {
|
||||
// @ts-ignore
|
||||
datasource.rowCount = value?.[0]?.COUNT
|
||||
} else {
|
||||
// @ts-ignore
|
||||
datasource.rowCount = value?.[0]?.count
|
||||
}
|
||||
}
|
||||
},
|
||||
datasource.rowCount
|
||||
@@ -339,12 +438,14 @@
|
||||
|
||||
async function insert() {
|
||||
try {
|
||||
const selected = resolvedConfig.type.selected
|
||||
await insertRowRunnable?.insertRow(
|
||||
resolvedConfig.type.configuration.postgresql.resource,
|
||||
resolvedConfig.type.configuration[selected].resource,
|
||||
$workspaceStore,
|
||||
resolvedConfig.type.configuration.postgresql.table,
|
||||
resolvedConfig.type.configuration[selected].table,
|
||||
resolvedConfig.columnDefs,
|
||||
args
|
||||
args,
|
||||
selected
|
||||
)
|
||||
|
||||
insertDrawer?.closeDrawer()
|
||||
@@ -368,11 +469,13 @@
|
||||
let getPrimaryKeysresolvedConfig = resolvedConfig.columnDefs?.filter((x) =>
|
||||
primaryColumns.includes(x.field)
|
||||
)
|
||||
const selected = resolvedConfig.type.selected
|
||||
deleteRow?.triggerDelete(
|
||||
resolvedConfig.type.configuration.postgresql.resource,
|
||||
resolvedConfig.type.configuration.postgresql.table ?? 'unknown',
|
||||
resolvedConfig.type.configuration[selected].resource,
|
||||
resolvedConfig.type.configuration[selected].table ?? 'unknown',
|
||||
getPrimaryKeysresolvedConfig,
|
||||
data
|
||||
data,
|
||||
selected
|
||||
)
|
||||
}
|
||||
|
||||
@@ -408,13 +511,18 @@
|
||||
/>
|
||||
{/if}
|
||||
<UpdateCell {id} bind:this={updateCell} />
|
||||
<DbExplorerCount
|
||||
renderCount={refreshCount}
|
||||
{id}
|
||||
{quicksearch}
|
||||
table={resolvedConfig?.type?.configuration?.postgresql?.table ?? ''}
|
||||
resource={resolvedConfig?.type?.configuration?.postgresql?.resource ?? ''}
|
||||
/>
|
||||
{#if render}
|
||||
<DbExplorerCount
|
||||
renderCount={refreshCount}
|
||||
{id}
|
||||
{quicksearch}
|
||||
table={resolvedConfig?.type?.configuration?.[resolvedConfig?.type?.selected]?.table ?? ''}
|
||||
resource={resolvedConfig?.type?.configuration?.[resolvedConfig?.type?.selected]?.resource ?? ''}
|
||||
resourceType={resolvedConfig?.type?.selected}
|
||||
columnDefs={resolvedConfig?.columnDefs}
|
||||
whereClause={resolvedConfig?.whereClause}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<InitializeComponent {id} />
|
||||
|
||||
@@ -430,9 +538,7 @@
|
||||
>
|
||||
<div class="h-full" bind:clientHeight={componentContainerHeight}>
|
||||
<div class="flex p-2 justify-between gap-4" bind:clientHeight={buttonContainerHeight}>
|
||||
<input
|
||||
on:pointerdown|stopPropagation
|
||||
on:keydown|stopPropagation
|
||||
<DebouncedInput
|
||||
class="w-full max-w-[300px]"
|
||||
type="text"
|
||||
bind:value={quicksearch}
|
||||
@@ -450,10 +556,10 @@
|
||||
Insert
|
||||
</Button>
|
||||
</div>
|
||||
{#if resolvedConfig.type.configuration?.postgresql?.resource && resolvedConfig.type.configuration?.postgresql?.table}
|
||||
{#if resolvedConfig.type.configuration?.[resolvedConfig?.type?.selected]?.resource && resolvedConfig.type.configuration?.[resolvedConfig?.type?.selected]?.table}
|
||||
<!-- {JSON.stringify(lastInput)} -->
|
||||
<!-- <span class="text-xs">{JSON.stringify(configuration.columnDefs)}</span> -->
|
||||
{#key renderCount}
|
||||
{#key renderCount && render}
|
||||
<!-- {JSON.stringify(resolvedConfig.columnDefs)} -->
|
||||
<AppAggridExplorerTable
|
||||
bind:this={aggrid}
|
||||
@@ -479,7 +585,12 @@
|
||||
<Button color="dark" size="xs" on:click={insert} disabled={!isInsertable}>Insert</Button>
|
||||
</svelte:fragment>
|
||||
|
||||
<InsertRow bind:args bind:isInsertable columnDefs={resolvedConfig.columnDefs} />
|
||||
<InsertRow
|
||||
bind:args
|
||||
bind:isInsertable
|
||||
columnDefs={resolvedConfig.columnDefs}
|
||||
dbType={resolvedConfig.type.selected}
|
||||
/>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
</Portal>
|
||||
|
||||
@@ -5,13 +5,17 @@
|
||||
import type RunnableComponent from '../../helpers/RunnableComponent.svelte'
|
||||
import RunnableWrapper from '../../helpers/RunnableWrapper.svelte'
|
||||
import { initOutput } from '../../../editor/appUtils'
|
||||
import { getCountPostgresql } from './utils'
|
||||
import { type ColumnDef, type DbType } from './utils'
|
||||
import { getCountInput } from './queries/count'
|
||||
|
||||
export let id: string
|
||||
export let table: string
|
||||
export let resource: string
|
||||
export let renderCount: number
|
||||
export let quicksearch: string
|
||||
export let resourceType: string
|
||||
export let columnDefs: ColumnDef[]
|
||||
export let whereClause: string | undefined
|
||||
|
||||
const { worldStore } = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
@@ -23,14 +27,13 @@
|
||||
|
||||
let runnableComponent: RunnableComponent
|
||||
let loading = false
|
||||
|
||||
let input: AppInput | undefined = undefined
|
||||
|
||||
$: table && renderCount != undefined && quicksearch != undefined && computeCount()
|
||||
|
||||
let lastTableCount = ''
|
||||
let renderCountLast = -1
|
||||
let quicksearchLast: string | undefined = undefined
|
||||
|
||||
$: table && renderCount != undefined && quicksearch != undefined && computeCount()
|
||||
|
||||
async function computeCount() {
|
||||
if (
|
||||
lastTableCount === table &&
|
||||
@@ -47,7 +50,7 @@
|
||||
}
|
||||
|
||||
async function getCount(resource: string, table: string, quicksearch: string) {
|
||||
input = getCountPostgresql(resource, table)
|
||||
input = getCountInput(resource, table, resourceType as DbType, columnDefs, whereClause)
|
||||
|
||||
await tick()
|
||||
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
import type RunnableComponent from '../../helpers/RunnableComponent.svelte'
|
||||
import RunnableWrapper from '../../helpers/RunnableWrapper.svelte'
|
||||
import { initOutput } from '../../../editor/appUtils'
|
||||
import { type ColumnMetadata, createDeletePostgresInput } from './utils'
|
||||
import { type ColumnDef, type DbType } from './utils'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { getDeleteInput } from './queries/delete'
|
||||
|
||||
export let id: string
|
||||
|
||||
@@ -28,12 +29,13 @@
|
||||
export async function triggerDelete(
|
||||
resource: string,
|
||||
table: string,
|
||||
columns: ColumnMetadata[],
|
||||
data: Record<string, any>
|
||||
columns: ColumnDef[],
|
||||
data: Record<string, any>,
|
||||
dbType: DbType
|
||||
) {
|
||||
// const datatype = tableMetaData?.find((column) => column.isprimarykey)?.datatype
|
||||
|
||||
input = createDeletePostgresInput(resource, table, columns)
|
||||
input = getDeleteInput(resource, table, columns, dbType)
|
||||
|
||||
await tick()
|
||||
|
||||
|
||||
@@ -1,19 +1,33 @@
|
||||
<script lang="ts">
|
||||
import type { Schema, SchemaProperty } from '$lib/common'
|
||||
import LightweightSchemaForm from '$lib/components/LightweightSchemaForm.svelte'
|
||||
import { ColumnIdentity, type ColumnMetadata } from './utils'
|
||||
import {
|
||||
ColumnIdentity,
|
||||
getFieldType,
|
||||
type ColumnMetadata,
|
||||
type DbType,
|
||||
type ColumnDef
|
||||
} from './utils'
|
||||
|
||||
import wasmUrl from 'windmill-parser-wasm/windmill_parser_wasm_bg.wasm?url'
|
||||
import init, {
|
||||
parse_sql,
|
||||
parse_mysql,
|
||||
parse_bigquery,
|
||||
parse_snowflake,
|
||||
parse_mssql
|
||||
} from 'windmill-parser-wasm'
|
||||
import type { MainArgSignature } from '$lib/gen'
|
||||
import { makeInsertQuery } from './queries/insert'
|
||||
import { argSigToJsonSchemaType } from '$lib/infer'
|
||||
init(wasmUrl)
|
||||
|
||||
export let args: Record<string, any> = {}
|
||||
export let columnDefs: Array<
|
||||
{
|
||||
field: string
|
||||
ignored: boolean
|
||||
hideInsert: boolean
|
||||
overrideDefaultValue: boolean
|
||||
defaultUserValue: any
|
||||
defaultValueNull: boolean
|
||||
} & ColumnMetadata
|
||||
> = []
|
||||
export let dbType: DbType = 'postgresql'
|
||||
|
||||
let schema: Schema | undefined = undefined
|
||||
|
||||
export let columnDefs: Array<ColumnDef & ColumnMetadata> = []
|
||||
|
||||
type FieldMetadata = {
|
||||
type: string
|
||||
@@ -36,21 +50,7 @@
|
||||
const name = column.field
|
||||
const isPrimaryKey = column.isprimarykey
|
||||
const defaultValue = column.defaultValueNull ? null : column.defaultUserValue
|
||||
|
||||
const baseType = type.split('(')[0]
|
||||
const validTextTypes = ['character varying', 'text']
|
||||
const validNumberTypes = ['integer', 'bigint', 'numeric', 'double precision']
|
||||
const validDateTypes = ['date', 'timestamp without time zone', 'timestamp with time zone']
|
||||
|
||||
const fieldType = validTextTypes.includes(baseType)
|
||||
? 'text'
|
||||
: validNumberTypes.includes(baseType)
|
||||
? 'number'
|
||||
: baseType === 'boolean'
|
||||
? 'checkbox'
|
||||
: validDateTypes.includes(baseType)
|
||||
? 'date'
|
||||
: 'text'
|
||||
const fieldType = getFieldType(type, dbType)
|
||||
|
||||
return {
|
||||
type,
|
||||
@@ -63,35 +63,61 @@
|
||||
}
|
||||
}) as FieldMetadata[] | undefined
|
||||
|
||||
function builtSchema(fields: FieldMetadata[]): Schema {
|
||||
async function parseSQLArgs(code: string, dbType: DbType) {
|
||||
await init(wasmUrl)
|
||||
|
||||
let rawSchema = ''
|
||||
switch (dbType) {
|
||||
case 'mysql':
|
||||
rawSchema = parse_mysql(code)
|
||||
break
|
||||
case 'postgresql':
|
||||
rawSchema = parse_sql(code)
|
||||
break
|
||||
case 'bigquery':
|
||||
rawSchema = parse_bigquery(code)
|
||||
break
|
||||
case 'snowflake':
|
||||
rawSchema = parse_snowflake(code)
|
||||
break
|
||||
case 'ms_sql_server':
|
||||
rawSchema = parse_mssql(code)
|
||||
break
|
||||
default:
|
||||
throw new Error('Language not supported')
|
||||
}
|
||||
|
||||
const args: MainArgSignature = JSON.parse(rawSchema)
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
async function builtSchema(fields: FieldMetadata[], dbType: DbType) {
|
||||
const properties: { [name: string]: SchemaProperty } = {}
|
||||
const required: string[] = []
|
||||
|
||||
const insertCode = makeInsertQuery('ignoredtable', columnDefs, dbType)
|
||||
const args = await parseSQLArgs(insertCode, dbType)
|
||||
|
||||
fields.forEach((field) => {
|
||||
console.log(field)
|
||||
const schemaProperty: SchemaProperty = {
|
||||
type: field.fieldType
|
||||
type: 'string'
|
||||
}
|
||||
|
||||
switch (field.fieldType) {
|
||||
case 'number':
|
||||
schemaProperty.type = 'number'
|
||||
const extractedDefaultValue = field.defaultValue
|
||||
schemaProperty.default = extractedDefaultValue ? Number(extractedDefaultValue) : undefined
|
||||
break
|
||||
case 'checkbox':
|
||||
schemaProperty.type = 'boolean'
|
||||
const parsedArg = args.args.find((arg) => arg.name === field.name)
|
||||
if (parsedArg) {
|
||||
argSigToJsonSchemaType(parsedArg.typ, schemaProperty)
|
||||
}
|
||||
|
||||
if (field.defaultValue) {
|
||||
if (schemaProperty.type === 'number') {
|
||||
schemaProperty.default = field.defaultValue ? Number(field.defaultValue) : undefined
|
||||
} else if (schemaProperty.type === 'boolean') {
|
||||
schemaProperty.default = field.defaultValue?.toLocaleLowerCase() === 'true'
|
||||
break
|
||||
case 'date':
|
||||
schemaProperty.type = 'string'
|
||||
schemaProperty.format = 'date-time'
|
||||
} else {
|
||||
schemaProperty.default = field.defaultValue
|
||||
break
|
||||
case 'text':
|
||||
default:
|
||||
schemaProperty.type = 'string'
|
||||
schemaProperty.default = field.defaultValue
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
properties[field.name] = schemaProperty
|
||||
@@ -106,15 +132,15 @@
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
schema = {
|
||||
$schema: 'http://json-schema.org/draft-07/schema#',
|
||||
type: 'object',
|
||||
properties,
|
||||
required
|
||||
}
|
||||
} as Schema
|
||||
}
|
||||
|
||||
$: schema = builtSchema(fields ?? []) as Schema
|
||||
$: builtSchema(fields ?? [], dbType)
|
||||
|
||||
export let isInsertable: boolean = false
|
||||
|
||||
@@ -127,4 +153,6 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<LightweightSchemaForm {schema} bind:args />
|
||||
{#if schema}
|
||||
<LightweightSchemaForm {schema} bind:args />
|
||||
{/if}
|
||||
|
||||
+5
-3
@@ -5,8 +5,9 @@
|
||||
import type RunnableComponent from '../../helpers/RunnableComponent.svelte'
|
||||
import RunnableWrapper from '../../helpers/RunnableWrapper.svelte'
|
||||
import { initOutput } from '../../../editor/appUtils'
|
||||
import { type ColumnDef, createPostgresInsert } from './utils'
|
||||
import { type ColumnDef, type DbType } from './utils'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { getInsertInput } from './queries/insert'
|
||||
|
||||
export let id: string
|
||||
|
||||
@@ -29,13 +30,14 @@
|
||||
workspace: string | undefined,
|
||||
table: string | undefined,
|
||||
columns: ColumnDef[],
|
||||
values: Record<string, any>
|
||||
values: Record<string, any>,
|
||||
resourceType: string
|
||||
): Promise<boolean> {
|
||||
if (!resource || !table || !workspace) {
|
||||
return false
|
||||
}
|
||||
|
||||
input = createPostgresInsert(table, columns, resource)
|
||||
input = getInsertInput(table, columns, resource, resourceType as DbType)
|
||||
|
||||
await tick()
|
||||
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
import type RunnableComponent from '../../helpers/RunnableComponent.svelte'
|
||||
import RunnableWrapper from '../../helpers/RunnableWrapper.svelte'
|
||||
import { initOutput } from '../../../editor/appUtils'
|
||||
import { createUpdatePostgresInput, type ColumnMetadata, getPrimaryKeys } from './utils'
|
||||
import { getPrimaryKeys, type ColumnDef, type DbType } from './utils'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { getUpdateInput } from './queries/update'
|
||||
|
||||
export let id: string
|
||||
|
||||
@@ -20,24 +21,24 @@
|
||||
|
||||
let runnableComponent: RunnableComponent
|
||||
let loading = false
|
||||
|
||||
let input: AppInput | undefined = undefined
|
||||
|
||||
export async function triggerUpdate(
|
||||
resource: string,
|
||||
table: string,
|
||||
column: ColumnMetadata,
|
||||
allColumns: ColumnMetadata[],
|
||||
column: ColumnDef,
|
||||
allColumns: ColumnDef[],
|
||||
valueToUpdate: string,
|
||||
data: Record<string, any>,
|
||||
oldValue: string | undefined = undefined
|
||||
oldValue: string | undefined = undefined,
|
||||
dbType: DbType
|
||||
) {
|
||||
// const datatype = tableMetaData?.find((column) => column.isprimarykey)?.datatype
|
||||
|
||||
let primaryColumns = getPrimaryKeys(allColumns)
|
||||
let columns = allColumns?.filter((x) => primaryColumns.includes(x.field))
|
||||
|
||||
input = createUpdatePostgresInput(resource, table, column, columns)
|
||||
input = getUpdateInput(resource, table, column, columns, dbType)
|
||||
|
||||
await tick()
|
||||
|
||||
@@ -51,7 +52,7 @@
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
{ valueToUpdate, ...ndata },
|
||||
{ value_to_update: valueToUpdate, ...ndata },
|
||||
{
|
||||
done: (x) => {
|
||||
sendUserToast('Value updated', false)
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
import type { AppInput, RunnableByName } from '$lib/components/apps/inputType'
|
||||
import { Preview } from '$lib/gen'
|
||||
import { buildParameters, type DbType } from '../utils'
|
||||
import { getLanguageByResourceType, type ColumnDef, buildVisibleFieldList } from '../utils'
|
||||
|
||||
function makeCountQuery(
|
||||
dbType: DbType,
|
||||
table: string,
|
||||
whereClause: string | undefined = undefined,
|
||||
columnDefs: ColumnDef[]
|
||||
): string {
|
||||
const wherePrefix = ' WHERE '
|
||||
const andCondition = ' AND '
|
||||
let quicksearchCondition = ''
|
||||
let query = buildParameters(
|
||||
[{ field: 'quicksearch', datatype: dbType === 'bigquery' ? 'string' : 'text' }],
|
||||
dbType
|
||||
)
|
||||
|
||||
query += '\n'
|
||||
|
||||
if (whereClause) {
|
||||
quicksearchCondition = ` ${whereClause} AND `
|
||||
}
|
||||
|
||||
const filteredColumns = buildVisibleFieldList(columnDefs, dbType)
|
||||
|
||||
switch (dbType) {
|
||||
case 'mysql':
|
||||
if (filteredColumns.length > 0) {
|
||||
quicksearchCondition += ` (:quicksearch = '' OR CONCAT_WS(' ', ${filteredColumns.join(
|
||||
', '
|
||||
)}) LIKE CONCAT('%', :quicksearch, '%'))`
|
||||
} else {
|
||||
quicksearchCondition += ` (:quicksearch = '' OR 1 = 1)`
|
||||
}
|
||||
query += `SELECT COUNT(*) as count FROM \`${table}\``
|
||||
break
|
||||
case 'postgresql':
|
||||
if (filteredColumns.length > 0) {
|
||||
quicksearchCondition += `($1 = '' OR CONCAT(${filteredColumns.join(
|
||||
', '
|
||||
)}) ILIKE '%' || $1 || '%')`
|
||||
} else {
|
||||
quicksearchCondition += `($1 = '' OR 1 = 1)`
|
||||
}
|
||||
query += `SELECT COUNT(*) as count FROM "${table}"`
|
||||
break
|
||||
case 'ms_sql_server':
|
||||
if (filteredColumns.length > 0) {
|
||||
quicksearchCondition += `(@p1 = '' OR CONCAT(${filteredColumns.join(
|
||||
', +'
|
||||
)}) LIKE '%' + @p1 + '%')`
|
||||
} else {
|
||||
quicksearchCondition += `(@p1 = '' OR 1 = 1)`
|
||||
}
|
||||
query += `SELECT COUNT(*) as count FROM [${table}]`
|
||||
break
|
||||
case 'snowflake': {
|
||||
query = ''
|
||||
|
||||
if (filteredColumns.length > 0) {
|
||||
query += buildParameters(
|
||||
[
|
||||
{ field: 'quicksearch', datatype: 'text' },
|
||||
{ field: 'quicksearch', datatype: 'text' }
|
||||
],
|
||||
dbType
|
||||
)
|
||||
|
||||
query += '\n'
|
||||
|
||||
quicksearchCondition += `(? = '' OR CONCAT(${filteredColumns.join(
|
||||
', '
|
||||
)}) ILIKE '%' || ? || '%')`
|
||||
} else {
|
||||
query += buildParameters([{ field: 'quicksearch', datatype: 'text' }], dbType)
|
||||
|
||||
query += '\n'
|
||||
|
||||
quicksearchCondition += `(? = '' OR 1 = 1)`
|
||||
}
|
||||
|
||||
query += `SELECT COUNT(*) as count FROM ${table}`
|
||||
break
|
||||
}
|
||||
case 'bigquery': {
|
||||
if (filteredColumns.length > 0) {
|
||||
const searchClause = filteredColumns
|
||||
.map((col) => {
|
||||
const def = columnDefs.find((c) => c.field === col.slice(1, -1))
|
||||
if (
|
||||
def?.datatype === 'JSON' ||
|
||||
def?.datatype.startsWith('STRUCT') ||
|
||||
def?.datatype.startsWith('ARRAY')
|
||||
) {
|
||||
return `TO_JSON_STRING(${col})`
|
||||
}
|
||||
return `${col}`
|
||||
})
|
||||
.join(',')
|
||||
quicksearchCondition += `(@quicksearch = '' OR REGEXP_CONTAINS(CONCAT(${searchClause}), '(?i)' || @quicksearch))`
|
||||
} else {
|
||||
quicksearchCondition += `(@quicksearch = '' OR 1 = 1)`
|
||||
}
|
||||
query += `SELECT COUNT(*) as count FROM \`${table}\``
|
||||
break
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error('Unsupported database type:' + dbType)
|
||||
}
|
||||
|
||||
if (whereClause) {
|
||||
query += `${wherePrefix}${quicksearchCondition}`
|
||||
} else {
|
||||
query += dbType === 'ms_sql_server' && !whereClause ? wherePrefix : andCondition
|
||||
query += quicksearchCondition
|
||||
}
|
||||
|
||||
if (
|
||||
!whereClause &&
|
||||
(dbType === Preview.language.MYSQL ||
|
||||
dbType === 'postgresql' ||
|
||||
dbType === 'snowflake' ||
|
||||
dbType === 'bigquery')
|
||||
) {
|
||||
query = query.replace(`${andCondition}`, wherePrefix)
|
||||
}
|
||||
|
||||
return query
|
||||
}
|
||||
|
||||
export function getCountInput(
|
||||
resource: string,
|
||||
table: string,
|
||||
resourceType: DbType,
|
||||
columnDefs: ColumnDef[],
|
||||
whereClause: string | undefined
|
||||
): AppInput | undefined {
|
||||
if (!resource || !table || !columnDefs) {
|
||||
// Return undefined if resource or table is not defined
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
const query = makeCountQuery(resourceType, table, whereClause, columnDefs)
|
||||
|
||||
const updateRunnable: RunnableByName = {
|
||||
name: 'AppDbExplorer',
|
||||
type: 'runnableByName',
|
||||
inlineScript: {
|
||||
content: query,
|
||||
language: getLanguageByResourceType(resourceType),
|
||||
schema: {
|
||||
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
||||
properties: {
|
||||
database: {
|
||||
description: 'Database name',
|
||||
type: 'object',
|
||||
format: `resource-${resourceType}`
|
||||
}
|
||||
},
|
||||
required: ['database'],
|
||||
type: 'object'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const updateQuery: AppInput = {
|
||||
runnable: updateRunnable,
|
||||
fields: {
|
||||
database: {
|
||||
type: 'static',
|
||||
value: resource,
|
||||
fieldType: 'object',
|
||||
format: `resource-${resourceType}`
|
||||
}
|
||||
},
|
||||
type: 'runnable',
|
||||
fieldType: 'object'
|
||||
}
|
||||
|
||||
return updateQuery
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import type { AppInput, RunnableByName } from '$lib/components/apps/inputType'
|
||||
import { getLanguageByResourceType, type ColumnDef, buildParameters, type DbType } from '../utils'
|
||||
|
||||
function updateWithAllValues(table: string, columns: ColumnDef[], dbType: DbType) {
|
||||
let query = buildParameters(columns, dbType)
|
||||
|
||||
switch (dbType) {
|
||||
case 'postgresql': {
|
||||
const conditions = columns
|
||||
.map((c, i) => `${c.field} = $${i + 1}::text::${c.datatype} `)
|
||||
.join(' AND ')
|
||||
|
||||
query += `\nDELETE FROM ${table} WHERE ${conditions} RETURNING 1;`
|
||||
return query
|
||||
}
|
||||
case 'mysql': {
|
||||
const conditions = columns.map((c) => `${c.field} = :${c.field}`).join(' AND ')
|
||||
query += `\nDELETE FROM ${table} WHERE ${conditions}`
|
||||
return query
|
||||
}
|
||||
case 'ms_sql_server': {
|
||||
const conditions = columns.map((c, i) => `${c.field} = @p${i + 1} `).join(' AND ')
|
||||
query += `\nDELETE FROM ${table} WHERE ${conditions}`
|
||||
return query
|
||||
}
|
||||
case 'snowflake': {
|
||||
const conditions = columns.map((c, i) => `${c.field} = ? `).join(' AND ')
|
||||
query += `\nDELETE FROM ${table} WHERE ${conditions}`
|
||||
return query
|
||||
}
|
||||
case 'bigquery': {
|
||||
const conditions = columns.map((c, i) => `${c.field} = @${c.field}`).join(' AND ')
|
||||
query += `\nDELETE FROM ${table} WHERE ${conditions}`
|
||||
return query
|
||||
}
|
||||
default:
|
||||
throw new Error('Unsupported database type')
|
||||
}
|
||||
}
|
||||
|
||||
export function getDeleteInput(
|
||||
resource: string,
|
||||
table: string,
|
||||
columns: ColumnDef[],
|
||||
dbType: DbType
|
||||
): AppInput | undefined {
|
||||
if (!resource || !table) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const updateRunnable: RunnableByName = {
|
||||
name: 'AppDbExplorer',
|
||||
type: 'runnableByName',
|
||||
inlineScript: {
|
||||
content: updateWithAllValues(table, columns, dbType),
|
||||
language: getLanguageByResourceType(dbType),
|
||||
schema: {
|
||||
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
||||
properties: {},
|
||||
required: ['database'],
|
||||
type: 'object'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const updateQuery: AppInput = {
|
||||
runnable: updateRunnable,
|
||||
fields: {
|
||||
database: {
|
||||
type: 'static',
|
||||
value: resource,
|
||||
fieldType: 'object',
|
||||
format: `resource-${dbType}`
|
||||
}
|
||||
},
|
||||
type: 'runnable',
|
||||
fieldType: 'object'
|
||||
}
|
||||
|
||||
return updateQuery
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import type { AppInput } from '$lib/components/apps/inputType'
|
||||
import { buildParameters, type DbType } from '../utils'
|
||||
import { getLanguageByResourceType, type ColumnDef } from '../utils'
|
||||
|
||||
function formatInsertValues(columns: ColumnDef[], dbType: DbType, startIndex: number = 1): string {
|
||||
switch (dbType) {
|
||||
case 'mysql':
|
||||
return columns.map((c) => `:${c.field}`).join(', ')
|
||||
case 'postgresql':
|
||||
return columns.map((c, i) => `$${startIndex + i}::${c.datatype}`).join(', ')
|
||||
case 'ms_sql_server':
|
||||
return columns.map((c, i) => `@p${startIndex + i}`).join(', ')
|
||||
case 'snowflake':
|
||||
return columns.map(() => `?`).join(', ')
|
||||
case 'bigquery':
|
||||
return columns.map((c) => `@${c.field}`).join(', ')
|
||||
default:
|
||||
throw new Error('Unsupported database type')
|
||||
}
|
||||
}
|
||||
|
||||
function formatColumnNames(columns: ColumnDef[]): string {
|
||||
return columns.map((c) => c.field).join(', ')
|
||||
}
|
||||
|
||||
function formatDefaultValues(columns: ColumnDef[]): string {
|
||||
const defaultValues = columns
|
||||
.map((c) => {
|
||||
if (c.defaultValueNull) {
|
||||
return 'NULL'
|
||||
} else {
|
||||
return typeof c.defaultUserValue === 'string'
|
||||
? `'${c.defaultUserValue}'`
|
||||
: c.defaultUserValue
|
||||
}
|
||||
})
|
||||
.join(', ')
|
||||
|
||||
return defaultValues
|
||||
}
|
||||
|
||||
export function makeInsertQuery(table: string, columns: ColumnDef[], dbType: DbType) {
|
||||
if (!table) throw new Error('Table name is required')
|
||||
|
||||
const columnsInsert = columns.filter((x) => !x.hideInsert)
|
||||
const columnsDefault = columns.filter(
|
||||
(x) => x.hideInsert && (x.overrideDefaultValue || x.defaultvalue === null)
|
||||
)
|
||||
|
||||
const allInsertColumns = columnsInsert.concat(columnsDefault)
|
||||
let query = buildParameters(columnsInsert, dbType)
|
||||
|
||||
query += '\n'
|
||||
|
||||
const shouldInsertComma = columnsDefault.length > 0 && columnsInsert.length > 0
|
||||
const columnNames = formatColumnNames(allInsertColumns)
|
||||
const insertValues = formatInsertValues(columnsInsert, dbType)
|
||||
const defaultValues = formatDefaultValues(columnsDefault)
|
||||
const commaOrEmpty = shouldInsertComma ? ', ' : ''
|
||||
|
||||
query += `INSERT INTO ${table} (${columnNames}) VALUES (${insertValues}${commaOrEmpty}${defaultValues})`
|
||||
|
||||
return query
|
||||
}
|
||||
|
||||
export function getInsertInput(
|
||||
table: string,
|
||||
columns: ColumnDef[],
|
||||
resource: string,
|
||||
dbType: DbType
|
||||
): AppInput {
|
||||
return {
|
||||
runnable: {
|
||||
name: 'AppDbExplorer',
|
||||
type: 'runnableByName',
|
||||
inlineScript: {
|
||||
content: makeInsertQuery(table, columns, dbType),
|
||||
language: getLanguageByResourceType(dbType),
|
||||
schema: {
|
||||
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
||||
properties: {},
|
||||
required: ['database'],
|
||||
type: 'object'
|
||||
}
|
||||
}
|
||||
},
|
||||
fields: {
|
||||
database: {
|
||||
type: 'static',
|
||||
value: resource,
|
||||
fieldType: 'object',
|
||||
format: `resource-${dbType}`
|
||||
}
|
||||
},
|
||||
type: 'runnable',
|
||||
fieldType: 'object'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
import type { AppInput, RunnableByName } from '$lib/components/apps/inputType'
|
||||
import { buildParameters, type DbType } from '../utils'
|
||||
import { getLanguageByResourceType, type ColumnDef, buildVisibleFieldList } from '../utils'
|
||||
|
||||
function makeSnowflakeSelectQuery(
|
||||
table: string,
|
||||
columnDefs: ColumnDef[],
|
||||
whereClause: string | undefined,
|
||||
options?: { limit?: number; offset?: number }
|
||||
) {
|
||||
const limit = coerceToNumber(options?.limit) || 100
|
||||
const offset = coerceToNumber(options?.offset) || 0
|
||||
|
||||
const headers: Array<{
|
||||
field: string
|
||||
datatype: string
|
||||
}> = [
|
||||
{
|
||||
field: 'quicksearch',
|
||||
datatype: 'text'
|
||||
}
|
||||
]
|
||||
|
||||
let query = ''
|
||||
|
||||
query += '\n'
|
||||
|
||||
const filteredColumns = buildVisibleFieldList(columnDefs, 'snowflake')
|
||||
const selectClause = filteredColumns.join(', ')
|
||||
|
||||
query += `SELECT ${selectClause} FROM "${table}"`
|
||||
|
||||
const quicksearchCondition = [
|
||||
'LENGTH(?) = 0',
|
||||
...filteredColumns.map((column) => {
|
||||
headers.push({
|
||||
field: 'quicksearch',
|
||||
datatype: 'text'
|
||||
})
|
||||
|
||||
return `CONCAT(${column}) ILIKE CONCAT('%', ?, '%')`
|
||||
})
|
||||
].join(' OR ')
|
||||
|
||||
if (whereClause) {
|
||||
query += ` WHERE ${whereClause} AND (${quicksearchCondition})`
|
||||
} else {
|
||||
query += ` WHERE ${quicksearchCondition}`
|
||||
}
|
||||
|
||||
const orderBy = columnDefs.map((column) => {
|
||||
headers.push(
|
||||
{
|
||||
field: 'order_by',
|
||||
datatype: 'text'
|
||||
},
|
||||
{
|
||||
field: 'is_desc',
|
||||
datatype: 'boolean'
|
||||
},
|
||||
{
|
||||
field: 'order_by',
|
||||
datatype: 'text'
|
||||
},
|
||||
{
|
||||
field: 'is_desc',
|
||||
datatype: 'boolean'
|
||||
}
|
||||
)
|
||||
|
||||
return `CASE WHEN ? = '${column.field}' AND ? = FALSE THEN "${column.field}" END ASC,
|
||||
CASE WHEN ? = '${column.field}' AND ? = TRUE THEN "${column.field}" END DESC`
|
||||
})
|
||||
|
||||
query += ` ORDER BY ${orderBy.join(',\n')}`
|
||||
query += ` LIMIT ${limit} OFFSET ${offset}`
|
||||
query = buildParameters(headers, 'snowflake') + '\n' + query
|
||||
|
||||
return query
|
||||
}
|
||||
|
||||
function makeSelectQuery(
|
||||
table: string,
|
||||
columnDefs: ColumnDef[],
|
||||
whereClause: string | undefined,
|
||||
dbType: DbType,
|
||||
options?: { limit?: number; offset?: number }
|
||||
) {
|
||||
if (!table) throw new Error('Table name is required')
|
||||
let quicksearchCondition = ''
|
||||
|
||||
let query = buildParameters(
|
||||
[
|
||||
{ field: 'limit', datatype: dbType === 'bigquery' ? 'integer' : 'int' },
|
||||
{ field: 'offset', datatype: dbType === 'bigquery' ? 'integer' : 'int' },
|
||||
{ field: 'quicksearch', datatype: dbType === 'bigquery' ? 'string' : 'text' },
|
||||
{ field: 'order_by', datatype: 'bigquery' ? 'string' : 'text' },
|
||||
{ field: 'is_desc', datatype: 'bigquery' ? 'bool' : 'boolean' }
|
||||
],
|
||||
dbType
|
||||
)
|
||||
|
||||
query += '\n'
|
||||
|
||||
const filteredColumns = buildVisibleFieldList(columnDefs, dbType)
|
||||
const selectClause = filteredColumns.join(', ')
|
||||
|
||||
console.log(table)
|
||||
|
||||
switch (dbType) {
|
||||
case 'mysql': {
|
||||
const orderBy = columnDefs
|
||||
.map((column) => {
|
||||
return `
|
||||
CASE WHEN :order_by = '${column.field}' AND :is_desc IS false THEN \`${column.field}\` END,
|
||||
CASE WHEN :order_by = '${column.field}' AND :is_desc IS true THEN \`${column.field}\` END DESC`
|
||||
})
|
||||
.join(',\n')
|
||||
|
||||
quicksearchCondition = ` (:quicksearch = '' OR CONCAT_WS(' ', ${filteredColumns.join(
|
||||
', '
|
||||
)}) LIKE CONCAT('%', :quicksearch, '%'))`
|
||||
|
||||
query += `SELECT ${selectClause} FROM \`${table}\``
|
||||
query += ` WHERE ${whereClause ? `${whereClause} AND` : ''} ${quicksearchCondition}`
|
||||
query += ` ORDER BY ${orderBy}`
|
||||
query += ` LIMIT :limit OFFSET :offset`
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'postgresql': {
|
||||
const orderBy = `
|
||||
${columnDefs
|
||||
.map(
|
||||
(column) =>
|
||||
`
|
||||
(CASE WHEN $4 = '${column.field}' AND $5 IS false THEN "${column.field}"::text END),
|
||||
(CASE WHEN $4 = '${column.field}' AND $5 IS true THEN "${column.field}"::text END) DESC`
|
||||
)
|
||||
.join(',\n')}`
|
||||
|
||||
quicksearchCondition = ` ($3 = '' OR "${table}"::text ILIKE '%' || $3 || '%')`
|
||||
query += `SELECT ${filteredColumns
|
||||
.map((column) => `${column}::text`)
|
||||
.join(', ')} FROM "${table}"`
|
||||
query += ` WHERE ${whereClause ? `${whereClause} AND` : ''} ${quicksearchCondition}`
|
||||
query += ` ORDER BY ${orderBy}`
|
||||
query += ` LIMIT $1::INT OFFSET $2::INT`
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'ms_sql_server':
|
||||
// MSSQL uses CONCAT for string concatenation and supports OFFSET FETCH for pagination
|
||||
// Note: MSSQL does not have a built-in ILIKE function, so we use LIKE with a case-insensitive collation if needed
|
||||
const orderBy = columnDefs
|
||||
.map((column) => {
|
||||
return `
|
||||
(CASE WHEN @p4 = '${column.field}' AND @p5 = 0 THEN ${column.field} END) ASC,
|
||||
(CASE WHEN @p4 = '${column.field}' AND @p5 = 1 THEN ${column.field} END) DESC`
|
||||
})
|
||||
.join(',\n')
|
||||
|
||||
quicksearchCondition = ` (@p3 = '' OR CONCAT(${selectClause}) LIKE '%' + @p3 + '%')`
|
||||
|
||||
query += `SELECT ${selectClause} FROM ${table}`
|
||||
query += ` WHERE ${whereClause ? `${whereClause} AND` : ''} ${quicksearchCondition}`
|
||||
query += ` ORDER BY ${orderBy}`
|
||||
query += ` OFFSET @p2 ROWS FETCH NEXT @p1 ROWS ONLY`
|
||||
break
|
||||
case 'snowflake': {
|
||||
return makeSnowflakeSelectQuery(table, columnDefs, whereClause, options)
|
||||
}
|
||||
case 'bigquery': {
|
||||
const orderBy = columnDefs
|
||||
.map((column) => {
|
||||
if (
|
||||
column.datatype === 'JSON' ||
|
||||
column.datatype.startsWith('STRUCT') ||
|
||||
column.datatype.startsWith('ARRAY')
|
||||
) {
|
||||
return `
|
||||
(CASE WHEN @order_by = '${column.field}' AND @is_desc = false THEN TO_JSON_STRING(${column.field}) END) ASC,
|
||||
(CASE WHEN @order_by = '${column.field}' AND @is_desc = true THEN TO_JSON_STRING(${column.field}) END) DESC`
|
||||
}
|
||||
return `
|
||||
(CASE WHEN @order_by = '${column.field}' AND @is_desc = false THEN ${column.field} END) ASC,
|
||||
(CASE WHEN @order_by = '${column.field}' AND @is_desc = true THEN ${column.field} END) DESC`
|
||||
})
|
||||
.join(',\n')
|
||||
|
||||
const searchClause = filteredColumns
|
||||
.map((col) => {
|
||||
const def = columnDefs.find((c) => c.field === col.slice(1, -1))
|
||||
if (
|
||||
def?.datatype === 'JSON' ||
|
||||
def?.datatype.startsWith('STRUCT') ||
|
||||
def?.datatype.startsWith('ARRAY')
|
||||
) {
|
||||
return `TO_JSON_STRING(${col})`
|
||||
}
|
||||
return `${col}`
|
||||
})
|
||||
.join(',')
|
||||
quicksearchCondition = ` (@quicksearch = '' OR REGEXP_CONTAINS(CONCAT(${searchClause}), '(?i)' || @quicksearch))`
|
||||
|
||||
query += `SELECT ${selectClause} FROM ${table}`
|
||||
query += ` WHERE ${whereClause ? `${whereClause} AND` : ''} ${quicksearchCondition}`
|
||||
query += ` ORDER BY ${orderBy}`
|
||||
query += ` LIMIT @limit OFFSET @offset`
|
||||
break
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error('Unsupported database type')
|
||||
}
|
||||
|
||||
return query
|
||||
}
|
||||
|
||||
function coerceToNumber(value: any): number {
|
||||
if (typeof value === 'number') {
|
||||
return value
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
return parseInt(value, 10)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
export function getSelectInput(
|
||||
resource: string,
|
||||
table: string | undefined,
|
||||
columnDefs: ColumnDef[],
|
||||
whereClause: string | undefined,
|
||||
dbType: DbType,
|
||||
options?: { limit?: number; offset?: number }
|
||||
): AppInput | undefined {
|
||||
if (!resource || !table || !columnDefs) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (columnDefs.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const getRunnable: RunnableByName = {
|
||||
name: 'AppDbExplorer',
|
||||
type: 'runnableByName',
|
||||
inlineScript: {
|
||||
content: makeSelectQuery(table, columnDefs, whereClause, dbType, options),
|
||||
language: getLanguageByResourceType(dbType)
|
||||
}
|
||||
}
|
||||
|
||||
const getQuery: AppInput = {
|
||||
runnable: getRunnable,
|
||||
fields: {
|
||||
database: {
|
||||
type: 'static',
|
||||
value: resource,
|
||||
fieldType: 'object',
|
||||
format: `resource-${dbType}`
|
||||
}
|
||||
},
|
||||
type: 'runnable',
|
||||
fieldType: 'object'
|
||||
}
|
||||
|
||||
return getQuery
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import type { AppInput, RunnableByName } from '$lib/components/apps/inputType'
|
||||
import { getLanguageByResourceType, type ColumnDef, buildParameters, type DbType } from '../utils'
|
||||
|
||||
function updateWithAllValues(
|
||||
table: string,
|
||||
column: ColumnDef,
|
||||
columns: ColumnDef[],
|
||||
dbType: DbType
|
||||
) {
|
||||
let query = buildParameters(
|
||||
[
|
||||
{
|
||||
field: 'value_to_update',
|
||||
datatype: column.datatype
|
||||
},
|
||||
...columns
|
||||
],
|
||||
dbType
|
||||
)
|
||||
|
||||
query += `\n`
|
||||
|
||||
switch (dbType) {
|
||||
case 'postgresql': {
|
||||
const conditions = columns
|
||||
.map((c, i) => `${c.field} = $${i + 2}::text::${c.datatype} `)
|
||||
.join(' AND ')
|
||||
|
||||
query += `\nUPDATE ${table} SET ${column.field} = $1::text::${column.datatype} WHERE ${conditions} RETURNING 1`
|
||||
return query
|
||||
}
|
||||
case 'mysql': {
|
||||
const conditions = columns.map((c) => `${c.field} = :${c.field}`).join(' AND ')
|
||||
query += `\nUPDATE ${table} SET ${column.field} = :value_to_update WHERE ${conditions}`
|
||||
return query
|
||||
}
|
||||
case 'ms_sql_server': {
|
||||
const conditions = columns.map((c, i) => `${c.field} = @p${i + 2} `).join(' AND ')
|
||||
query += `\nUPDATE ${table} SET ${column.field} = @p1 WHERE ${conditions}`
|
||||
return query
|
||||
}
|
||||
case 'snowflake': {
|
||||
const conditions = columns.map((c, i) => `${c.field} = ? `).join(' AND ')
|
||||
query += `\nUPDATE ${table} SET ${column.field} = ? WHERE ${conditions}`
|
||||
return query
|
||||
}
|
||||
case 'bigquery': {
|
||||
const conditions = columns.map((c, i) => `${c.field} = @${c.field}`).join(' AND ')
|
||||
query += `\nUPDATE ${table} SET ${column.field} = @value_to_update WHERE ${conditions}`
|
||||
return query
|
||||
}
|
||||
default:
|
||||
throw new Error('Unsupported database type')
|
||||
}
|
||||
}
|
||||
|
||||
export function getUpdateInput(
|
||||
resource: string,
|
||||
table: string,
|
||||
column: ColumnDef,
|
||||
columns: ColumnDef[],
|
||||
dbType: DbType
|
||||
): AppInput | undefined {
|
||||
if (!resource || !table) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const updateRunnable: RunnableByName = {
|
||||
name: 'AppDbExplorer',
|
||||
type: 'runnableByName',
|
||||
inlineScript: {
|
||||
content: updateWithAllValues(table, column, columns, dbType),
|
||||
language: getLanguageByResourceType(dbType),
|
||||
schema: {
|
||||
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
||||
properties: {},
|
||||
required: ['database'],
|
||||
type: 'object'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const updateQuery: AppInput = {
|
||||
runnable: updateRunnable,
|
||||
fields: {
|
||||
database: {
|
||||
type: 'static',
|
||||
value: resource,
|
||||
fieldType: 'object',
|
||||
format: `resource-${dbType}`
|
||||
}
|
||||
},
|
||||
type: 'runnable',
|
||||
fieldType: 'object'
|
||||
}
|
||||
|
||||
return updateQuery
|
||||
}
|
||||
@@ -1,313 +1,8 @@
|
||||
import type { AppInput, RunnableByName } from '../../../inputType'
|
||||
import { JobService, Preview } from '$lib/gen'
|
||||
import type { DBSchema, DBSchemas, GraphqlSchema, SQLSchema } from '$lib/stores'
|
||||
import { buildClientSchema, getIntrospectionQuery, printSchema } from 'graphql'
|
||||
import { tryEvery } from '$lib/utils'
|
||||
|
||||
export function makeQuery(
|
||||
table: string,
|
||||
tableMetadata: TableMetadata,
|
||||
whereClause: string | undefined
|
||||
) {
|
||||
if (!table) throw new Error('Table name is required')
|
||||
|
||||
const filteredColumns = tableMetadata
|
||||
.filter((x) => x != undefined)
|
||||
.map((column) => `"${column?.field}"::text`)
|
||||
|
||||
let selectClause = filteredColumns.join(', ')
|
||||
|
||||
let orderBy = `
|
||||
${tableMetadata
|
||||
.map(
|
||||
(column) =>
|
||||
`
|
||||
(CASE WHEN $4 = '${column.field}' AND $5 IS false THEN "${column.field}"::text END),
|
||||
(CASE WHEN $4 = '${column.field}' AND $5 IS true THEN "${column.field}"::text END) DESC`
|
||||
)
|
||||
.join(',\n')}`
|
||||
|
||||
let query = `
|
||||
-- $1 limit
|
||||
-- $2 offset
|
||||
-- $3 quicksearch
|
||||
-- $4 orderBy
|
||||
-- $5 is_desc
|
||||
|
||||
SELECT ${selectClause} FROM "${table}" WHERE `
|
||||
if (whereClause) {
|
||||
query += ` ${whereClause} AND `
|
||||
}
|
||||
query += ` ($3 = '' OR "${table}"::text ILIKE '%' || $3 || '%')`
|
||||
|
||||
query += ` ORDER BY ${orderBy}`
|
||||
query += ` LIMIT $1::INT OFFSET $2::INT`
|
||||
|
||||
return query
|
||||
}
|
||||
|
||||
export function createPostgresInsert(
|
||||
table: string,
|
||||
columns: ColumnDef[],
|
||||
resource: string
|
||||
): AppInput {
|
||||
return {
|
||||
runnable: {
|
||||
name: 'AppDbExplorer',
|
||||
type: 'runnableByName',
|
||||
inlineScript: {
|
||||
content: makeInsertQuery(table, columns),
|
||||
language: Preview.language.POSTGRESQL,
|
||||
schema: {
|
||||
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
||||
properties: {},
|
||||
required: ['database'],
|
||||
type: 'object'
|
||||
}
|
||||
}
|
||||
},
|
||||
fields: {
|
||||
database: {
|
||||
type: 'static',
|
||||
value: resource,
|
||||
fieldType: 'object',
|
||||
format: 'resource-postgresql'
|
||||
}
|
||||
},
|
||||
type: 'runnable',
|
||||
fieldType: 'object'
|
||||
}
|
||||
}
|
||||
|
||||
export function makeInsertQuery(table: string, columns: ColumnDef[]) {
|
||||
if (!table) throw new Error('Table name is required')
|
||||
|
||||
const columnsInsert = columns.filter((x) => !x.hideInsert)
|
||||
const columnsDefault = columns.filter(
|
||||
(x) => x.hideInsert && (x.overrideDefaultValue || x.defaultvalue === null)
|
||||
)
|
||||
|
||||
const allInsertColumns = columnsInsert.concat(columnsDefault)
|
||||
// Constructing the query
|
||||
const query = `
|
||||
${columnsInsert.map((column, i) => `-- $${i + 1} ${column.field}`).join('\n')}
|
||||
|
||||
INSERT INTO ${table} (${allInsertColumns.map((c) => c.field).join(', ')})
|
||||
VALUES (${columnsInsert.map((c, i) => `$${i + 1}::${c.datatype}`).join(', ')}${
|
||||
columnsDefault.length > 0 ? ',' : ''
|
||||
} ${columnsDefault
|
||||
.map((c) => (c.defaultValueNull ? 'NULL' : `${c.defaultUserValue}::${c.datatype}`))
|
||||
.join(', ')})`
|
||||
|
||||
return query
|
||||
}
|
||||
|
||||
export function getPrimaryKeys(tableMetadata?: TableMetadata): string[] {
|
||||
let r = tableMetadata?.filter((x) => x.isprimarykey)?.map((x) => x.field) ?? []
|
||||
if (r?.length === 0) {
|
||||
r = tableMetadata?.map((x) => x.field) ?? []
|
||||
}
|
||||
return r ?? []
|
||||
}
|
||||
|
||||
export function createPostgresInput(
|
||||
resource: string,
|
||||
table: string | undefined,
|
||||
columns: TableMetadata,
|
||||
whereClause: string | undefined
|
||||
): AppInput | undefined {
|
||||
if (!resource || !table || !columns) {
|
||||
// Return undefined if resource or table is not defined
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (columns.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const getRunnable: RunnableByName = {
|
||||
name: 'AppDbExplorer',
|
||||
type: 'runnableByName',
|
||||
inlineScript: {
|
||||
content: makeQuery(table, columns, whereClause),
|
||||
language: Preview.language.POSTGRESQL
|
||||
}
|
||||
}
|
||||
|
||||
const getQuery: AppInput = {
|
||||
runnable: getRunnable,
|
||||
fields: {
|
||||
database: {
|
||||
type: 'static',
|
||||
value: resource,
|
||||
fieldType: 'object',
|
||||
format: 'resource-postgresql'
|
||||
}
|
||||
},
|
||||
type: 'runnable',
|
||||
fieldType: 'object'
|
||||
}
|
||||
|
||||
return getQuery
|
||||
}
|
||||
|
||||
export function createUpdatePostgresInput(
|
||||
resource: string,
|
||||
table: string,
|
||||
column: ColumnMetadata,
|
||||
columns: ColumnMetadata[]
|
||||
): AppInput | undefined {
|
||||
if (!resource || !table) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const query = updateWithAllValues()
|
||||
|
||||
function updateWithAllValues() {
|
||||
let query = `
|
||||
-- $1 valueToUpdate
|
||||
${columns.map((c, i) => `-- $${i + 2} ${c.field}`).join('\n')}
|
||||
|
||||
UPDATE ${table} SET ${column.field} = $1::text::${column.datatype} WHERE
|
||||
${columns.map((c, i) => `${c.field} = $${i + 2}::text::${c.datatype} `).join(' AND ')}
|
||||
RETURNING 1`
|
||||
|
||||
return query
|
||||
}
|
||||
|
||||
const updateRunnable: RunnableByName = {
|
||||
name: 'AppDbExplorer',
|
||||
type: 'runnableByName',
|
||||
inlineScript: {
|
||||
content: query,
|
||||
language: Preview.language.POSTGRESQL,
|
||||
schema: {
|
||||
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
||||
properties: {},
|
||||
required: ['database'],
|
||||
type: 'object'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const updateQuery: AppInput = {
|
||||
runnable: updateRunnable,
|
||||
fields: {
|
||||
database: {
|
||||
type: 'static',
|
||||
value: resource,
|
||||
fieldType: 'object',
|
||||
format: 'resource-postgresql'
|
||||
}
|
||||
},
|
||||
type: 'runnable',
|
||||
fieldType: 'object'
|
||||
}
|
||||
|
||||
return updateQuery
|
||||
}
|
||||
|
||||
export function createDeletePostgresInput(
|
||||
resource: string,
|
||||
table: string,
|
||||
columns: ColumnMetadata[]
|
||||
): AppInput | undefined {
|
||||
if (!resource || !table) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const query = updateWithAllValues()
|
||||
|
||||
function updateWithAllValues() {
|
||||
let query = `
|
||||
${columns.map((c, i) => `-- $${i + 1} ${c.field}`).join('\n')}
|
||||
|
||||
DELETE FROM ${table} WHERE ${columns
|
||||
.map((c, i) => `${c.field} = $${i + 1}::text::${c.datatype}`)
|
||||
.join(' AND ')} RETURNING 1;`
|
||||
|
||||
return query
|
||||
}
|
||||
|
||||
const updateRunnable: RunnableByName = {
|
||||
name: 'AppDbExplorer',
|
||||
type: 'runnableByName',
|
||||
inlineScript: {
|
||||
content: query,
|
||||
language: Preview.language.POSTGRESQL,
|
||||
schema: {
|
||||
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
||||
properties: {},
|
||||
required: ['database'],
|
||||
type: 'object'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const updateQuery: AppInput = {
|
||||
runnable: updateRunnable,
|
||||
fields: {
|
||||
database: {
|
||||
type: 'static',
|
||||
value: resource,
|
||||
fieldType: 'object',
|
||||
format: 'resource-postgresql'
|
||||
}
|
||||
},
|
||||
type: 'runnable',
|
||||
fieldType: 'object'
|
||||
}
|
||||
|
||||
return updateQuery
|
||||
}
|
||||
|
||||
export function getCountPostgresql(resource: string, table: string): AppInput | undefined {
|
||||
if (!resource || !table) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const query = `
|
||||
-- $1 quicksearch
|
||||
SELECT COUNT(*) FROM ${table} WHERE ($1 = '' OR ${table}::text ILIKE '%' || $1 || '%')`
|
||||
|
||||
const updateRunnable: RunnableByName = {
|
||||
name: 'AppDbExplorer',
|
||||
type: 'runnableByName',
|
||||
inlineScript: {
|
||||
content: query,
|
||||
language: Preview.language.POSTGRESQL,
|
||||
schema: {
|
||||
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
||||
properties: {
|
||||
database: {
|
||||
description: 'Database name',
|
||||
type: 'object',
|
||||
format: 'resource-postgresql'
|
||||
}
|
||||
},
|
||||
required: ['database'],
|
||||
type: 'object'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const updateQuery: AppInput = {
|
||||
runnable: updateRunnable,
|
||||
fields: {
|
||||
database: {
|
||||
type: 'static',
|
||||
value: resource,
|
||||
fieldType: 'object',
|
||||
format: 'resource-postgresql'
|
||||
}
|
||||
},
|
||||
type: 'runnable',
|
||||
fieldType: 'object'
|
||||
}
|
||||
|
||||
return updateQuery
|
||||
}
|
||||
|
||||
export enum ColumnIdentity {
|
||||
ByDefault = 'By Default',
|
||||
Always = 'Always',
|
||||
@@ -353,41 +48,113 @@ export type ColumnDef = {
|
||||
export async function loadTableMetaData(
|
||||
resource: string,
|
||||
workspace: string | undefined,
|
||||
table: string | undefined
|
||||
table: string | undefined,
|
||||
resourceType: string
|
||||
): Promise<TableMetadata | undefined> {
|
||||
if (!resource || !table || !workspace) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const code = `
|
||||
SELECT
|
||||
a.attname as field,
|
||||
pg_catalog.format_type(a.atttypid, a.atttypmod) as DataType,
|
||||
(SELECT substring(pg_catalog.pg_get_expr(d.adbin, d.adrelid, true) for 128)
|
||||
FROM pg_catalog.pg_attrdef d
|
||||
WHERE d.adrelid = a.attrelid AND d.adnum = a.attnum AND a.atthasdef) as DefaultValue,
|
||||
(SELECT CASE WHEN i.indisprimary THEN true ELSE 'NO' END
|
||||
FROM pg_catalog.pg_class tbl, pg_catalog.pg_class idx, pg_catalog.pg_index i, pg_catalog.pg_attribute att
|
||||
WHERE tbl.oid = a.attrelid AND idx.oid = i.indexrelid AND att.attrelid = tbl.oid
|
||||
AND i.indrelid = tbl.oid AND att.attnum = any(i.indkey) AND att.attname = a.attname LIMIT 1) as IsPrimaryKey,
|
||||
CASE a.attidentity
|
||||
WHEN 'd' THEN 'By Default'
|
||||
WHEN 'a' THEN 'Always'
|
||||
ELSE 'No'
|
||||
END as IsIdentity,
|
||||
CASE a.attnotnull
|
||||
WHEN false THEN 'YES'
|
||||
ELSE 'NO'
|
||||
END as IsNullable,
|
||||
(SELECT true
|
||||
FROM pg_catalog.pg_enum e
|
||||
WHERE e.enumtypid = a.atttypid FETCH FIRST ROW ONLY) as IsEnum
|
||||
FROM pg_catalog.pg_attribute a
|
||||
WHERE a.attrelid = (SELECT oid FROM pg_catalog.pg_class WHERE relname = '${table}')
|
||||
AND a.attnum > 0 AND NOT a.attisdropped
|
||||
ORDER BY a.attnum;
|
||||
let code: string = ''
|
||||
|
||||
`
|
||||
if (resourceType === 'mysql') {
|
||||
code = `
|
||||
SELECT
|
||||
COLUMN_NAME as field,
|
||||
COLUMN_TYPE as DataType,
|
||||
COLUMN_DEFAULT as DefaultValue,
|
||||
CASE WHEN COLUMN_KEY = 'PRI' THEN 'YES' ELSE 'NO' END as IsPrimaryKey,
|
||||
CASE WHEN EXTRA like '%auto_increment%' THEN 'YES' ELSE 'NO' END as IsIdentity,
|
||||
CASE WHEN IS_NULLABLE = 'YES' THEN 'YES' ELSE 'NO' END as IsNullable,
|
||||
CASE WHEN DATA_TYPE = 'enum' THEN true ELSE false END as IsEnum
|
||||
FROM
|
||||
INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE
|
||||
TABLE_NAME = '${table}'
|
||||
ORDER BY
|
||||
ORDINAL_POSITION;
|
||||
`
|
||||
} else if (resourceType === 'postgresql') {
|
||||
code = `
|
||||
SELECT
|
||||
a.attname as field,
|
||||
pg_catalog.format_type(a.atttypid, a.atttypmod) as DataType,
|
||||
(SELECT substring(pg_catalog.pg_get_expr(d.adbin, d.adrelid, true) for 128)
|
||||
FROM pg_catalog.pg_attrdef d
|
||||
WHERE d.adrelid = a.attrelid AND d.adnum = a.attnum AND a.atthasdef) as DefaultValue,
|
||||
(SELECT CASE WHEN i.indisprimary THEN true ELSE 'NO' END
|
||||
FROM pg_catalog.pg_class tbl, pg_catalog.pg_class idx, pg_catalog.pg_index i, pg_catalog.pg_attribute att
|
||||
WHERE tbl.oid = a.attrelid AND idx.oid = i.indexrelid AND att.attrelid = tbl.oid
|
||||
AND i.indrelid = tbl.oid AND att.attnum = any(i.indkey) AND att.attname = a.attname LIMIT 1) as IsPrimaryKey,
|
||||
CASE a.attidentity
|
||||
WHEN 'd' THEN 'By Default'
|
||||
WHEN 'a' THEN 'Always'
|
||||
ELSE 'No'
|
||||
END as IsIdentity,
|
||||
CASE a.attnotnull
|
||||
WHEN false THEN 'YES'
|
||||
ELSE 'NO'
|
||||
END as IsNullable,
|
||||
(SELECT true
|
||||
FROM pg_catalog.pg_enum e
|
||||
WHERE e.enumtypid = a.atttypid FETCH FIRST ROW ONLY) as IsEnum
|
||||
FROM pg_catalog.pg_attribute a
|
||||
WHERE a.attrelid = (SELECT oid FROM pg_catalog.pg_class WHERE relname = '${table}')
|
||||
AND a.attnum > 0 AND NOT a.attisdropped
|
||||
ORDER BY a.attnum;
|
||||
|
||||
`
|
||||
} else if (resourceType === 'ms_sql_server') {
|
||||
code = `
|
||||
SELECT
|
||||
COLUMN_NAME as field,
|
||||
DATA_TYPE as DataType,
|
||||
COLUMN_DEFAULT as DefaultValue,
|
||||
CASE WHEN COLUMNPROPERTY(OBJECT_ID(TABLE_NAME), COLUMN_NAME, 'IsIdentity') = 1 THEN 'By Default' ELSE 'No' END as IsIdentity,
|
||||
CASE WHEN COLUMNPROPERTY(OBJECT_ID(TABLE_NAME), COLUMN_NAME, 'IsIdentity') = 1 THEN 1 ELSE 0 END as IsPrimaryKey, -- This line still needs correction for primary key identification
|
||||
CASE WHEN IS_NULLABLE = 'YES' THEN 'YES' ELSE 'NO' END as IsNullable,
|
||||
CASE WHEN DATA_TYPE = 'enum' THEN 1 ELSE 0 END as IsEnum
|
||||
FROM
|
||||
INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE
|
||||
TABLE_NAME = '${table}'
|
||||
ORDER BY
|
||||
ORDINAL_POSITION;
|
||||
|
||||
`
|
||||
} else if (resourceType === 'snowflake') {
|
||||
code = `
|
||||
select COLUMN_NAME as field,
|
||||
DATA_TYPE as DataType,
|
||||
COLUMN_DEFAULT as DefaultValue,
|
||||
CASE WHEN COLUMN_DEFAULT like 'AUTOINCREMENT%' THEN 'By Default' ELSE 'No' END as IsIdentity,
|
||||
CASE WHEN COLUMN_DEFAULT like 'AUTOINCREMENT%' THEN 1 ELSE 0 END as IsPrimaryKey,
|
||||
CASE WHEN IS_NULLABLE = 'YES' THEN 'YES' ELSE 'NO' END as IsNullable,
|
||||
CASE WHEN DATA_TYPE = 'enum' THEN 1 ELSE 0 END as IsEnum
|
||||
from information_schema.columns
|
||||
where table_name = '${table}'
|
||||
order by ORDINAL_POSITION;
|
||||
`
|
||||
} else if (resourceType === 'bigquery') {
|
||||
code = `SELECT
|
||||
c.COLUMN_NAME as field,
|
||||
DATA_TYPE as DataType,
|
||||
CASE WHEN COLUMN_DEFAULT = 'NULL' THEN '' ELSE COLUMN_DEFAULT END as DefaultValue,
|
||||
CASE WHEN constraint_name is not null THEN true ELSE false END as IsPrimaryKey,
|
||||
'No' as IsIdentity,
|
||||
IS_NULLABLE as IsNullable,
|
||||
false as IsEnum
|
||||
FROM
|
||||
test_dataset.INFORMATION_SCHEMA.COLUMNS c
|
||||
LEFT JOIN
|
||||
test_dataset.INFORMATION_SCHEMA.KEY_COLUMN_USAGE p
|
||||
on c.table_name = p.table_name AND c.column_name = p.COLUMN_NAME
|
||||
WHERE
|
||||
c.TABLE_NAME = "${table.split('.')[1]}"
|
||||
order by c.ORDINAL_POSITION;`
|
||||
} else {
|
||||
throw new Error('Unsupported database type:' + resourceType)
|
||||
}
|
||||
|
||||
const maxRetries = 3
|
||||
let attempts = 0
|
||||
@@ -397,7 +164,7 @@ ORDER BY a.attnum;
|
||||
const job = await JobService.runScriptPreview({
|
||||
workspace: workspace,
|
||||
requestBody: {
|
||||
language: Preview.language.POSTGRESQL,
|
||||
language: getLanguageByResourceType(resourceType),
|
||||
content: code,
|
||||
args: {
|
||||
database: resource
|
||||
@@ -415,7 +182,11 @@ ORDER BY a.attnum;
|
||||
if (testResult.success) {
|
||||
attempts = maxRetries
|
||||
|
||||
return testResult.result
|
||||
if (resourceType === 'ms_sql_server') {
|
||||
return testResult.result[0]
|
||||
} else {
|
||||
return testResult.result
|
||||
}
|
||||
} else {
|
||||
attempts++
|
||||
}
|
||||
@@ -457,6 +228,7 @@ const scripts: Record<
|
||||
acc[table_schema].push(a)
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
const data = {}
|
||||
for (const key in schemas) {
|
||||
data[key] = schemas[key].reduce((acc, a) => {
|
||||
@@ -478,13 +250,14 @@ const scripts: Record<
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
|
||||
return data
|
||||
},
|
||||
lang: 'postgresql',
|
||||
argName: 'database'
|
||||
},
|
||||
mysql: {
|
||||
code: "select TABLE_SCHEMA, TABLE_NAME, DATA_TYPE, COLUMN_NAME, COLUMN_DEFAULT from information_schema.columns where table_schema != 'information_schema'",
|
||||
code: "SELECT DATABASE() AS default_db_name, TABLE_SCHEMA, TABLE_NAME, DATA_TYPE, COLUMN_NAME, COLUMN_DEFAULT FROM information_schema.columns WHERE table_schema = DATABASE() OR table_schema NOT IN ('information_schema', 'mysql', 'performance_schema', 'sys', '_vt');",
|
||||
processingFn: (rows) => {
|
||||
const schemas = rows.reduce((acc, a) => {
|
||||
const table_schema = a.TABLE_SCHEMA
|
||||
@@ -493,6 +266,7 @@ const scripts: Record<
|
||||
acc[table_schema].push(a)
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
const data = {}
|
||||
for (const key in schemas) {
|
||||
data[key] = schemas[key].reduce((acc, a) => {
|
||||
@@ -525,7 +299,7 @@ const scripts: Record<
|
||||
argName: 'api'
|
||||
},
|
||||
bigquery: {
|
||||
code: `import { BigQuery } from '@google-cloud/bigquery';
|
||||
code: `import { BigQuery } from '@google-cloud/bigquery@7.5.0';
|
||||
export async function main(args: bigquery) {
|
||||
const bq = new BigQuery({
|
||||
credentials: args
|
||||
@@ -657,6 +431,7 @@ export async function getDbSchemas(
|
||||
const { processingFn } = scripts[resourceType]
|
||||
const schema =
|
||||
processingFn !== undefined ? processingFn(testResult.result) : testResult.result
|
||||
|
||||
dbSchemas[resourcePath] = {
|
||||
lang: resourceTypeToLang(resourceType) as SQLSchema['lang'],
|
||||
schema,
|
||||
@@ -715,73 +490,160 @@ export function formatGraphqlSchema(dbSchema: GraphqlSchema): string {
|
||||
return printSchema(buildClientSchema(dbSchema.schema))
|
||||
}
|
||||
|
||||
/**
|
||||
* Base class for embedding a svelte component within an AGGrid call.
|
||||
* See: https://stackoverflow.com/a/72608215
|
||||
*/
|
||||
import type { ICellRendererComp, ICellRendererParams } from 'ag-grid-community'
|
||||
export function getFieldType(type: string, databaseType: DbType) {
|
||||
switch (databaseType) {
|
||||
case 'postgresql': {
|
||||
const baseType = type.split('(')[0]
|
||||
const validTextTypes = ['character varying', 'text']
|
||||
const validNumberTypes = ['integer', 'bigint', 'numeric', 'double precision']
|
||||
const validDateTypes = ['date', 'timestamp without time zone', 'timestamp with time zone']
|
||||
|
||||
/**
|
||||
* Class for defining a cell renderer.
|
||||
* If you don't need to define a separate class you could use cellRendererFactory
|
||||
* to create a component with the column definitions.
|
||||
*/
|
||||
export abstract class AbstractCellRenderer implements ICellRendererComp {
|
||||
eGui: any
|
||||
protected value: any
|
||||
protected params: any
|
||||
constructor(parentElement = 'span') {
|
||||
// create empty span (or other element) to place svelte component in
|
||||
this.eGui = document.createElement(parentElement)
|
||||
}
|
||||
return validTextTypes.includes(baseType)
|
||||
? 'text'
|
||||
: validNumberTypes.includes(baseType)
|
||||
? 'number'
|
||||
: baseType === 'boolean'
|
||||
? 'checkbox'
|
||||
: validDateTypes.includes(baseType)
|
||||
? 'date'
|
||||
: 'text'
|
||||
}
|
||||
case 'mysql': {
|
||||
const baseType = type.split('(')[0].toLowerCase() // Ensure case-insensitive comparison
|
||||
const validTextTypes = ['varchar', 'text', 'char', 'mediumtext', 'longtext', 'tinytext']
|
||||
const validNumberTypes = ['int', 'bigint', 'decimal', 'numeric', 'float', 'double']
|
||||
const validDateTypes = ['date', 'datetime', 'timestamp', 'time', 'year']
|
||||
|
||||
init(params: ICellRendererParams & { onClick?: (data: any) => void }) {
|
||||
this.value = params.value
|
||||
this.createComponent(params)
|
||||
this.eGui.addEventListener('click', () => params.onClick?.(params.data))
|
||||
this.params = params
|
||||
}
|
||||
return validTextTypes.includes(baseType)
|
||||
? 'text'
|
||||
: validNumberTypes.includes(baseType)
|
||||
? 'number'
|
||||
: baseType === 'boolean'
|
||||
? 'checkbox'
|
||||
: validDateTypes.includes(baseType)
|
||||
? 'date'
|
||||
: 'text'
|
||||
}
|
||||
case 'ms_sql_server': {
|
||||
const baseType = type.split('(')[0].toLowerCase() // Ensure case-insensitive comparison
|
||||
const validTextTypes = ['varchar', 'text', 'char', 'nchar', 'nvarchar', 'ntext']
|
||||
const validNumberTypes = [
|
||||
'int',
|
||||
'bigint',
|
||||
'decimal',
|
||||
'numeric',
|
||||
'float',
|
||||
'real',
|
||||
'smallint',
|
||||
'tinyint'
|
||||
]
|
||||
const validDateTypes = [
|
||||
'date',
|
||||
'datetime',
|
||||
'datetime2',
|
||||
'smalldatetime',
|
||||
'datetimeoffset',
|
||||
'time'
|
||||
]
|
||||
|
||||
getGui() {
|
||||
return this.eGui
|
||||
}
|
||||
return validTextTypes.includes(baseType)
|
||||
? 'text'
|
||||
: validNumberTypes.includes(baseType)
|
||||
? 'number'
|
||||
: baseType === 'bit'
|
||||
? 'checkbox'
|
||||
: validDateTypes.includes(baseType)
|
||||
? 'date'
|
||||
: 'text'
|
||||
}
|
||||
|
||||
refresh(params: ICellRendererParams) {
|
||||
this.value = params.value
|
||||
this.eGui.innerHTML = ''
|
||||
case 'snowflake': {
|
||||
const baseType = type.split('(')[0].toLowerCase() // Ensure case-insensitive comparison
|
||||
const validTextTypes = ['varchar', 'text', 'char']
|
||||
const validNumberTypes = ['int', 'number', 'decimal', 'float', 'double']
|
||||
const validDateTypes = ['date', 'timestamp', 'time']
|
||||
|
||||
return true
|
||||
}
|
||||
return validTextTypes.includes(baseType)
|
||||
? 'text'
|
||||
: validNumberTypes.includes(baseType)
|
||||
? 'number'
|
||||
: baseType === 'boolean'
|
||||
? 'checkbox'
|
||||
: validDateTypes.includes(baseType)
|
||||
? 'date'
|
||||
: 'text'
|
||||
}
|
||||
|
||||
/**
|
||||
* Define and create the svelte component to use in the cell
|
||||
* @example
|
||||
* // This is all you need to do within this method: create the component with new, specify the target
|
||||
* // is the class, and pass in props via the params.
|
||||
* new CampusIcon({
|
||||
* target: this.eGui,
|
||||
* props: {
|
||||
* color: params.data?.color,
|
||||
* name: params.data?.name
|
||||
* }
|
||||
* @param params params for rendering the call, including the value for the cell
|
||||
*/
|
||||
abstract createComponent(params: ICellRendererParams): void
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a cell renderer using the given callback for how to initialise a svelte component.
|
||||
* See AbstractCellRenderer.createComponent
|
||||
* @param svelteComponent function for how to create the svelte component
|
||||
* @returns
|
||||
*/
|
||||
export function cellRendererFactory(
|
||||
svelteComponent: (cell: AbstractCellRenderer, params: ICellRendererParams) => void
|
||||
) {
|
||||
class Renderer extends AbstractCellRenderer {
|
||||
createComponent(params: ICellRendererParams<any, any>): void {
|
||||
svelteComponent(this, params)
|
||||
default: {
|
||||
return 'text'
|
||||
}
|
||||
}
|
||||
return Renderer
|
||||
}
|
||||
|
||||
export type DbType = 'mysql' | 'ms_sql_server' | 'postgresql' | 'snowflake' | 'bigquery'
|
||||
|
||||
export function buildVisibleFieldList(columnDefs: ColumnDef[], dbType: DbType) {
|
||||
// Filter out hidden columns to avoid counting the wrong number of rows
|
||||
return columnDefs
|
||||
.filter((columnDef: ColumnDef) => columnDef && columnDef.hide !== true)
|
||||
.map((column) => {
|
||||
switch (dbType) {
|
||||
case 'postgresql':
|
||||
return `"${column?.field}"` // PostgreSQL uses double quotes for identifiers
|
||||
case 'ms_sql_server':
|
||||
return `[${column?.field}]` // MSSQL uses square brackets for identifiers
|
||||
case 'mysql':
|
||||
return `\`${column?.field}\`` // MySQL uses backticks
|
||||
case 'snowflake':
|
||||
return `"${column?.field}"` // Snowflake uses double quotes for identifiers
|
||||
case 'bigquery':
|
||||
return `\`${column?.field}\`` // BigQuery uses backticks
|
||||
default:
|
||||
throw new Error('Unsupported database type')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function getLanguageByResourceType(name: string) {
|
||||
const language = {
|
||||
postgresql: Preview.language.POSTGRESQL,
|
||||
mysql: Preview.language.MYSQL,
|
||||
ms_sql_server: Preview.language.MSSQL,
|
||||
snowflake: Preview.language.SNOWFLAKE,
|
||||
bigquery: Preview.language.BIGQUERY
|
||||
}
|
||||
return language[name]
|
||||
}
|
||||
|
||||
export function buildParameters(
|
||||
columns: Array<{
|
||||
field: string
|
||||
datatype: string
|
||||
}>,
|
||||
databaseType: string
|
||||
) {
|
||||
return columns
|
||||
.map((column, i) => {
|
||||
switch (databaseType) {
|
||||
case 'postgresql':
|
||||
return `-- $${i + 1} ${column.field}`
|
||||
case 'mysql':
|
||||
return `-- :${column.field} (${column.datatype.split('(')[0]})`
|
||||
case 'ms_sql_server':
|
||||
return `-- @p${i + 1} ${column.field} (${column.datatype.split('(')[0]})`
|
||||
case 'snowflake':
|
||||
return `-- ? ${column.field} (${column.datatype.split('(')[0]})`
|
||||
case 'bigquery':
|
||||
return `-- @${column.field} (${column.datatype.split('(')[0]})`
|
||||
}
|
||||
})
|
||||
.join('\n')
|
||||
}
|
||||
|
||||
export function getPrimaryKeys(tableMetadata?: TableMetadata): string[] {
|
||||
let r = tableMetadata?.filter((x) => x.isprimarykey)?.map((x) => x.field) ?? []
|
||||
if (r?.length === 0) {
|
||||
r = tableMetadata?.map((x) => x.field) ?? []
|
||||
}
|
||||
return r ?? []
|
||||
}
|
||||
|
||||
+13
-17
@@ -16,12 +16,11 @@
|
||||
import type { Output } from '$lib/components/apps/rx'
|
||||
import type { InitConfig } from '$lib/components/apps/editor/appUtils'
|
||||
import { Button } from '$lib/components/common'
|
||||
import { cellRendererFactory } from '../dbtable/utils'
|
||||
import { cellRendererFactory } from './utils'
|
||||
import { Trash2 } from 'lucide-svelte'
|
||||
// import 'ag-grid-community/dist/styles/ag-theme-alpine-dark.css'
|
||||
|
||||
export let id: string
|
||||
|
||||
export let customCss: ComponentCustomCSS<'aggridcomponent'> | undefined = undefined
|
||||
export let containerHeight: number | undefined = undefined
|
||||
export let resolvedConfig: InitConfig<
|
||||
@@ -135,11 +134,13 @@
|
||||
new Button({
|
||||
target: c.eGui,
|
||||
props: {
|
||||
btnClasses: 'mt-1',
|
||||
color: 'red',
|
||||
variant: 'border',
|
||||
btnClasses: 'w-12',
|
||||
wrapperClasses: 'flex justify-end items-center h-full',
|
||||
color: 'light',
|
||||
size: 'sm',
|
||||
variant: 'contained',
|
||||
iconOnly: true,
|
||||
endIcon: { icon: Trash2 },
|
||||
startIcon: { icon: Trash2 },
|
||||
nonCaptureEvent: true
|
||||
}
|
||||
})
|
||||
@@ -174,7 +175,7 @@
|
||||
editable: resolvedConfig?.allEditable,
|
||||
onCellValueChanged
|
||||
},
|
||||
infiniteInitialRowCount: 1000,
|
||||
infiniteInitialRowCount: 100,
|
||||
cacheBlockSize: 100,
|
||||
cacheOverflowSize: 2,
|
||||
maxBlocksInCache: 20,
|
||||
@@ -222,9 +223,8 @@
|
||||
|
||||
let oldDatasource = datasource
|
||||
$: if (datasource && datasource != oldDatasource) {
|
||||
console.log('datasource changed')
|
||||
|
||||
oldDatasource = datasource
|
||||
|
||||
api?.updateGridOptions({ datasource })
|
||||
}
|
||||
|
||||
@@ -283,11 +283,7 @@
|
||||
|
||||
{#if Array.isArray(resolvedConfig.columnDefs) && resolvedConfig.columnDefs.every(isObject)}
|
||||
<div
|
||||
class={twMerge(
|
||||
'border shadow-sm divide-y flex flex-col h-full',
|
||||
css?.container?.class,
|
||||
'wm-aggrid-container'
|
||||
)}
|
||||
class={twMerge('divide-y flex flex-col h-full', css?.container?.class, 'wm-aggrid-container')}
|
||||
style={containerHeight ? `height: ${containerHeight}px;` : css?.container?.style}
|
||||
bind:clientHeight
|
||||
bind:clientWidth
|
||||
@@ -303,10 +299,10 @@
|
||||
>
|
||||
<div bind:this={eGui} style:height="100%" />
|
||||
</div>
|
||||
<div class="flex gap-1 w-full justify-end text-sm text-secondary py-1"
|
||||
>{firstRow}{'->'}{lastRow + 1} of {datasource?.rowCount} rows</div
|
||||
>
|
||||
</div>
|
||||
<div class="flex gap-1 absolute bottom-1 right-2 text-sm text-secondary"
|
||||
>{firstRow}{'->'}{lastRow + 1} of {datasource?.rowCount} rows</div
|
||||
>
|
||||
{:else if resolvedConfig.columnDefs != undefined}
|
||||
<Alert title="Parsing issues" type="error" size="xs">
|
||||
The columnDefs should be an array of objects, received:
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Base class for embedding a svelte component within an AGGrid call.
|
||||
* See: https://stackoverflow.com/a/72608215
|
||||
*/
|
||||
import type { ICellRendererComp, ICellRendererParams } from 'ag-grid-community'
|
||||
|
||||
/**
|
||||
* Class for defining a cell renderer.
|
||||
* If you don't need to define a separate class you could use cellRendererFactory
|
||||
* to create a component with the column definitions.
|
||||
*/
|
||||
export abstract class AbstractCellRenderer implements ICellRendererComp {
|
||||
eGui: any
|
||||
protected value: any
|
||||
protected params: any
|
||||
constructor(parentElement = 'span') {
|
||||
// create empty span (or other element) to place svelte component in
|
||||
this.eGui = document.createElement(parentElement)
|
||||
}
|
||||
|
||||
init(params: ICellRendererParams & { onClick?: (data: any) => void }) {
|
||||
this.value = params.value
|
||||
this.createComponent(params)
|
||||
this.eGui.addEventListener('click', () => params.onClick?.(params.data))
|
||||
this.params = params
|
||||
}
|
||||
|
||||
getGui() {
|
||||
return this.eGui
|
||||
}
|
||||
|
||||
refresh(params: ICellRendererParams) {
|
||||
this.value = params.value
|
||||
this.eGui.innerHTML = ''
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Define and create the svelte component to use in the cell
|
||||
* @example
|
||||
* // This is all you need to do within this method: create the component with new, specify the target
|
||||
* // is the class, and pass in props via the params.
|
||||
* new CampusIcon({
|
||||
* target: this.eGui,
|
||||
* props: {
|
||||
* color: params.data?.color,
|
||||
* name: params.data?.name
|
||||
* }
|
||||
* @param params params for rendering the call, including the value for the cell
|
||||
*/
|
||||
abstract createComponent(params: ICellRendererParams): void
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a cell renderer using the given callback for how to initialise a svelte component.
|
||||
* See AbstractCellRenderer.createComponent
|
||||
* @param svelteComponent function for how to create the svelte component
|
||||
* @returns
|
||||
*/
|
||||
export function cellRendererFactory(
|
||||
svelteComponent: (cell: AbstractCellRenderer, params: ICellRendererParams) => void
|
||||
) {
|
||||
class Renderer extends AbstractCellRenderer {
|
||||
createComponent(params: ICellRendererParams<any, any>): void {
|
||||
svelteComponent(this, params)
|
||||
}
|
||||
}
|
||||
return Renderer
|
||||
}
|
||||
@@ -75,15 +75,13 @@
|
||||
import { cloneDeep } from 'lodash'
|
||||
import AppReportsDrawer from './AppReportsDrawer.svelte'
|
||||
import HighlightCode from '$lib/components/HighlightCode.svelte'
|
||||
import {
|
||||
type ColumnDef,
|
||||
getCountPostgresql,
|
||||
createPostgresInput,
|
||||
createPostgresInsert,
|
||||
createUpdatePostgresInput,
|
||||
getPrimaryKeys
|
||||
} from '../components/display/dbtable/utils'
|
||||
import { type ColumnDef, getPrimaryKeys } from '../components/display/dbtable/utils'
|
||||
import DebugPanel from './contextPanel/DebugPanel.svelte'
|
||||
import { getCountInput } from '../components/display/dbtable/queries/count'
|
||||
import { getSelectInput } from '../components/display/dbtable/queries/select'
|
||||
import { getInsertInput } from '../components/display/dbtable/queries/insert'
|
||||
import { getUpdateInput } from '../components/display/dbtable/queries/update'
|
||||
import { getDeleteInput } from '../components/display/dbtable/queries/delete'
|
||||
|
||||
async function hash(message) {
|
||||
try {
|
||||
@@ -192,8 +190,12 @@
|
||||
if (c.type === 'dbexplorercomponent') {
|
||||
let nr: { id: string; input: AppInput }[] = []
|
||||
let config = c.configuration as any
|
||||
let pg = config?.type?.configuration?.postgresql
|
||||
if (pg) {
|
||||
|
||||
const dbType = config?.type?.selected
|
||||
|
||||
let pg = config?.type?.configuration?.[dbType]
|
||||
|
||||
if (pg && dbType) {
|
||||
const { table, resource } = pg
|
||||
const tableValue = table.value
|
||||
const resourceValue = resource.value
|
||||
@@ -203,17 +205,22 @@
|
||||
| undefined
|
||||
if (tableValue && resourceValue && columnDefs) {
|
||||
r.push({
|
||||
input: createPostgresInput(resourceValue, tableValue, columnDefs, whereClause),
|
||||
input: getSelectInput(resourceValue, tableValue, columnDefs, whereClause, dbType),
|
||||
id: x.id
|
||||
})
|
||||
r.push({
|
||||
input: getCountPostgresql(resourceValue, tableValue),
|
||||
input: getCountInput(resourceValue, tableValue, dbType, columnDefs, whereClause),
|
||||
id: x.id + '_count'
|
||||
})
|
||||
r.push({
|
||||
input: createPostgresInsert(tableValue, columnDefs, resourceValue),
|
||||
input: getInsertInput(tableValue, columnDefs, resourceValue, dbType),
|
||||
id: x.id + '_insert'
|
||||
})
|
||||
r.push({
|
||||
input: getDeleteInput(resourceValue, tableValue, columnDefs, dbType),
|
||||
id: x.id + '_delete'
|
||||
})
|
||||
|
||||
let primaryColumns = getPrimaryKeys(columnDefs)
|
||||
let columns = columnDefs?.filter((x) => primaryColumns.includes(x.field))
|
||||
|
||||
@@ -221,7 +228,7 @@
|
||||
.filter((col) => col.editable || config.allEditable.value)
|
||||
.forEach((column) => {
|
||||
r.push({
|
||||
input: createUpdatePostgresInput(resourceValue, tableValue, column, columns),
|
||||
input: getUpdateInput(resourceValue, tableValue, column, columns, dbType),
|
||||
id: x.id + '_update'
|
||||
})
|
||||
})
|
||||
@@ -247,6 +254,8 @@
|
||||
)
|
||||
)) as ([string, Record<string, any>] | undefined)[]
|
||||
|
||||
console.log('allTriggers', allTriggers)
|
||||
|
||||
policy.triggerables = Object.fromEntries(
|
||||
allTriggers.filter(Boolean) as [string, Record<string, any>][]
|
||||
)
|
||||
@@ -259,7 +268,8 @@
|
||||
): Promise<[string, Record<string, any>] | undefined> {
|
||||
const staticInputs = collectStaticFields(fields)
|
||||
if (runnable?.type == 'runnableByName') {
|
||||
console.log(runnable.inlineScript?.content)
|
||||
console.log('processRunnable:content', runnable.inlineScript?.content)
|
||||
|
||||
let hex = await hash(runnable.inlineScript?.content)
|
||||
console.log('hex', hex, id)
|
||||
return [`${id}:rawscript/${hex}`, staticInputs]
|
||||
|
||||
@@ -3358,13 +3358,77 @@ See date-fns format for more information. By default, it is 'dd.MM.yyyy HH:mm'
|
||||
selected: 'postgresql',
|
||||
labels: {
|
||||
postgresql: 'PostgreSQL',
|
||||
msql: 'MySQL'
|
||||
mysql: 'MySQL',
|
||||
ms_sql_server: 'MS SQL Server',
|
||||
snowflake: 'Snowflake',
|
||||
bigquery: 'BigQuery'
|
||||
},
|
||||
configuration: {
|
||||
postgresql: {
|
||||
resource: {
|
||||
type: 'static',
|
||||
fieldType: 'resource',
|
||||
subFieldType: 'postgres',
|
||||
value: ''
|
||||
} as StaticAppInput,
|
||||
table: {
|
||||
fieldType: 'select',
|
||||
subfieldType: 'db-table',
|
||||
type: 'static',
|
||||
selectOptions: [],
|
||||
value: undefined
|
||||
}
|
||||
},
|
||||
mysql: {
|
||||
resource: {
|
||||
type: 'static',
|
||||
fieldType: 'resource',
|
||||
subFieldType: 'mysql',
|
||||
value: ''
|
||||
} as StaticAppInput,
|
||||
table: {
|
||||
fieldType: 'select',
|
||||
subfieldType: 'db-table',
|
||||
type: 'static',
|
||||
selectOptions: [],
|
||||
value: undefined
|
||||
}
|
||||
},
|
||||
ms_sql_server: {
|
||||
resource: {
|
||||
type: 'static',
|
||||
fieldType: 'resource',
|
||||
subFieldType: 'ms_sql_server',
|
||||
value: ''
|
||||
} as StaticAppInput,
|
||||
table: {
|
||||
fieldType: 'select',
|
||||
subfieldType: 'db-table',
|
||||
type: 'static',
|
||||
selectOptions: [],
|
||||
value: undefined
|
||||
}
|
||||
},
|
||||
snowflake: {
|
||||
resource: {
|
||||
type: 'static',
|
||||
fieldType: 'resource',
|
||||
subFieldType: 'snowflake',
|
||||
value: ''
|
||||
} as StaticAppInput,
|
||||
table: {
|
||||
fieldType: 'select',
|
||||
subfieldType: 'db-table',
|
||||
type: 'static',
|
||||
selectOptions: [],
|
||||
value: undefined
|
||||
}
|
||||
},
|
||||
bigquery: {
|
||||
resource: {
|
||||
type: 'static',
|
||||
fieldType: 'resource',
|
||||
subFieldType: 'bigquery',
|
||||
value: ''
|
||||
} as StaticAppInput,
|
||||
table: {
|
||||
|
||||
+2
-2
@@ -67,7 +67,7 @@
|
||||
<IconSelectInput bind:componentInput />
|
||||
{:else if fieldType === 'tab-select'}
|
||||
<TabSelectInput bind:componentInput />
|
||||
{:else if fieldType === 'resource' && subFieldType !== 's3'}
|
||||
{:else if fieldType === 'resource' && subFieldType && ['mysql', 'postgres', 'ms_sql_server', 'snowflake', 'bigquery'].includes(subFieldType)}
|
||||
<ResourcePicker
|
||||
initialValue={componentInput.value?.split('$res:')?.[1] || ''}
|
||||
on:change={(e) => {
|
||||
@@ -81,7 +81,7 @@
|
||||
}
|
||||
}}
|
||||
showSchemaExplorer
|
||||
resourceType="postgresql"
|
||||
resourceType={subFieldType === 'postgres' ? 'postgresql' : subFieldType}
|
||||
/>
|
||||
{:else if fieldType === 'resource' && subFieldType === 's3'}
|
||||
<ResourcePicker
|
||||
|
||||
@@ -33,6 +33,12 @@ export type InputType =
|
||||
| 'db-table'
|
||||
| 's3'
|
||||
| 'number-tuple'
|
||||
// Used for selecting the right resource type in the Database Studio
|
||||
| 'postgres'
|
||||
| 'mysql'
|
||||
| 'ms_sql_server'
|
||||
| 'snowflake'
|
||||
| 'bigquery'
|
||||
|
||||
// Connection to an output of another component
|
||||
// defined by the id of the component and the path of the output
|
||||
@@ -209,6 +215,11 @@ export type AppInput =
|
||||
| AppInputSpec<'array', object[], 'ag-chart'>
|
||||
| AppInputSpec<'resource', string>
|
||||
| AppInputSpec<'resource', string, 's3'>
|
||||
| AppInputSpec<'resource', string, 'postgres'>
|
||||
| AppInputSpec<'resource', string, 'mysql'>
|
||||
| AppInputSpec<'resource', string, 'ms_sql_server'>
|
||||
| AppInputSpec<'resource', string, 'snowflake'>
|
||||
| AppInputSpec<'resource', string, 'bigquery'>
|
||||
| AppInputSpec<'array', object[], 'number-tuple'>
|
||||
|
||||
export type RowAppInput = Extract<AppInput, { type: 'row' }>
|
||||
|
||||
@@ -63,15 +63,19 @@ export type Configuration =
|
||||
| TemplateV2AppInput
|
||||
|
||||
export type StaticConfiguration = GeneralAppInput & StaticAppInput
|
||||
export type RichConfigurationT<T> =
|
||||
| (T & { type: AppInput['type'] })
|
||||
| {
|
||||
type: 'oneOf'
|
||||
selected: string
|
||||
tooltip?: string
|
||||
labels?: Record<string, string>
|
||||
configuration: Record<string, Record<string, T>>
|
||||
}
|
||||
export type OneOfRichConfiguration<T> = {
|
||||
type: 'oneOf'
|
||||
selected: string
|
||||
tooltip?: string
|
||||
labels?: Record<string, string>
|
||||
configuration: Record<string, Record<string, T>>
|
||||
}
|
||||
|
||||
export type OneOfConfiguration = OneOfRichConfiguration<
|
||||
GeneralAppInput & (StaticAppInput | EvalAppInput | EvalV2AppInput)
|
||||
>
|
||||
|
||||
export type RichConfigurationT<T> = (T & { type: AppInput['type'] }) | OneOfRichConfiguration<T>
|
||||
export type RichConfiguration = RichConfigurationT<Configuration>
|
||||
export type RichConfigurations = Record<string, RichConfiguration>
|
||||
|
||||
|
||||
@@ -133,7 +133,7 @@
|
||||
ButtonType.FontSizeClasses[size],
|
||||
ButtonType.SpacingClasses[spacingSize][variant],
|
||||
'focus-visible:ring-2 font-semibold',
|
||||
dropdownItems ? 'rounded-l-md h-full' : 'rounded-md',
|
||||
dropdownItems && dropdownItems.length > 0 ? 'rounded-l-md h-full' : 'rounded-md',
|
||||
'justify-center items-center text-center whitespace-nowrap inline-flex gap-2',
|
||||
btnClasses,
|
||||
'active:opacity-80 transition-all',
|
||||
|
||||
@@ -151,7 +151,7 @@ export async function inferArgs(
|
||||
await tick()
|
||||
}
|
||||
|
||||
function argSigToJsonSchemaType(
|
||||
export function argSigToJsonSchemaType(
|
||||
t:
|
||||
| string
|
||||
| { resource: string | null }
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import { getUserExt } from '$lib/user'
|
||||
import DarkModeToggle from '$lib/components/sidebar/DarkModeToggle.svelte'
|
||||
import type { Schedule } from '$lib/components/flows/scheduleUtils'
|
||||
|
||||
let token = $page.url.searchParams.get('wm_token') ?? undefined
|
||||
let workspace = $page.url.searchParams.get('workspace') ?? undefined
|
||||
@@ -62,12 +63,14 @@
|
||||
|
||||
let initialCode = JSON.stringify($flowStore, null, 4)
|
||||
const flowStateStore = writable({} as FlowState)
|
||||
const scheduleStore = writable({
|
||||
const scheduleStore = writable<Schedule>({
|
||||
args: {},
|
||||
cron: '',
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
enabled: false
|
||||
enabled: false,
|
||||
summary: undefined
|
||||
})
|
||||
|
||||
const previewArgsStore = writable<Record<string, any>>({})
|
||||
const scriptEditorDrawer = writable(undefined)
|
||||
const moving = writable<{ module: FlowModule; modules: FlowModule[] } | undefined>(undefined)
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
// vite.config.js
|
||||
import { sveltekit } from "file:///git/windmill/frontend/node_modules/@sveltejs/kit/src/exports/vite/index.js";
|
||||
import { readFileSync } from "fs";
|
||||
import { fileURLToPath } from "url";
|
||||
import monacoEditorPlugin from "file:///git/windmill/frontend/node_modules/vite-plugin-monaco-editor/dist/index.js";
|
||||
import circleDependency from "file:///git/windmill/frontend/node_modules/vite-plugin-circular-dependency/dist/index.mjs";
|
||||
var __vite_injected_original_import_meta_url = "file:///git/windmill/frontend/vite.config.js";
|
||||
var file = fileURLToPath(new URL("package.json", __vite_injected_original_import_meta_url));
|
||||
var json = readFileSync(file, "utf8");
|
||||
var version = JSON.parse(json);
|
||||
var config = {
|
||||
server: {
|
||||
port: 3e3,
|
||||
proxy: {
|
||||
"^/api/.*": {
|
||||
target: process.env.REMOTE ?? "https://app.windmill.dev/",
|
||||
changeOrigin: true,
|
||||
cookieDomainRewrite: "localhost"
|
||||
},
|
||||
"^/ws/.*": {
|
||||
target: process.env.REMOTE_LSP ?? "https://app.windmill.dev",
|
||||
changeOrigin: true,
|
||||
ws: true
|
||||
},
|
||||
"^/ws_mp/.*": {
|
||||
target: process.env.REMOTE_MP ?? "https://app.windmill.dev",
|
||||
changeOrigin: true,
|
||||
ws: true
|
||||
}
|
||||
}
|
||||
},
|
||||
preview: {
|
||||
port: 3e3
|
||||
},
|
||||
plugins: [
|
||||
sveltekit(),
|
||||
monacoEditorPlugin.default({
|
||||
publicPath: "workers",
|
||||
languageWorkers: [],
|
||||
customWorkers: [
|
||||
{
|
||||
label: "graphql",
|
||||
entry: "monaco-graphql/esm/graphql.worker"
|
||||
}
|
||||
]
|
||||
}),
|
||||
circleDependency({ circleImportThrowErr: false })
|
||||
],
|
||||
define: {
|
||||
__pkg__: version
|
||||
},
|
||||
optimizeDeps: {
|
||||
include: ["highlight.js", "highlight.js/lib/core"]
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
path: "path-browserify"
|
||||
},
|
||||
dedupe: ["monaco-editor", "vscode"]
|
||||
},
|
||||
assetsInclude: ["**/*.wasm"]
|
||||
};
|
||||
var vite_config_default = config;
|
||||
export {
|
||||
vite_config_default as default
|
||||
};
|
||||
//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcuanMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvZ2l0L3dpbmRtaWxsL2Zyb250ZW5kXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCIvZ2l0L3dpbmRtaWxsL2Zyb250ZW5kL3ZpdGUuY29uZmlnLmpzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9naXQvd2luZG1pbGwvZnJvbnRlbmQvdml0ZS5jb25maWcuanNcIjtpbXBvcnQgeyBzdmVsdGVraXQgfSBmcm9tICdAc3ZlbHRlanMva2l0L3ZpdGUnXG5pbXBvcnQgeyByZWFkRmlsZVN5bmMgfSBmcm9tICdmcydcbmltcG9ydCB7IGZpbGVVUkxUb1BhdGggfSBmcm9tICd1cmwnXG5pbXBvcnQgbW9uYWNvRWRpdG9yUGx1Z2luIGZyb20gJ3ZpdGUtcGx1Z2luLW1vbmFjby1lZGl0b3InXG5pbXBvcnQgY2lyY2xlRGVwZW5kZW5jeSBmcm9tICd2aXRlLXBsdWdpbi1jaXJjdWxhci1kZXBlbmRlbmN5J1xuXG5jb25zdCBmaWxlID0gZmlsZVVSTFRvUGF0aChuZXcgVVJMKCdwYWNrYWdlLmpzb24nLCBpbXBvcnQubWV0YS51cmwpKVxuY29uc3QganNvbiA9IHJlYWRGaWxlU3luYyhmaWxlLCAndXRmOCcpXG5jb25zdCB2ZXJzaW9uID0gSlNPTi5wYXJzZShqc29uKVxuXG4vKiogQHR5cGUge2ltcG9ydCgndml0ZScpLlVzZXJDb25maWd9ICovXG5jb25zdCBjb25maWcgPSB7XG5cdHNlcnZlcjoge1xuXHRcdHBvcnQ6IDMwMDAsXG5cdFx0cHJveHk6IHtcblx0XHRcdCdeL2FwaS8uKic6IHtcblx0XHRcdFx0dGFyZ2V0OiBwcm9jZXNzLmVudi5SRU1PVEUgPz8gJ2h0dHBzOi8vYXBwLndpbmRtaWxsLmRldi8nLFxuXHRcdFx0XHRjaGFuZ2VPcmlnaW46IHRydWUsXG5cdFx0XHRcdGNvb2tpZURvbWFpblJld3JpdGU6ICdsb2NhbGhvc3QnXG5cdFx0XHR9LFxuXHRcdFx0J14vd3MvLionOiB7XG5cdFx0XHRcdHRhcmdldDogcHJvY2Vzcy5lbnYuUkVNT1RFX0xTUCA/PyAnaHR0cHM6Ly9hcHAud2luZG1pbGwuZGV2Jyxcblx0XHRcdFx0Y2hhbmdlT3JpZ2luOiB0cnVlLFxuXHRcdFx0XHR3czogdHJ1ZVxuXHRcdFx0fSxcblx0XHRcdCdeL3dzX21wLy4qJzoge1xuXHRcdFx0XHR0YXJnZXQ6IHByb2Nlc3MuZW52LlJFTU9URV9NUCA/PyAnaHR0cHM6Ly9hcHAud2luZG1pbGwuZGV2Jyxcblx0XHRcdFx0Y2hhbmdlT3JpZ2luOiB0cnVlLFxuXHRcdFx0XHR3czogdHJ1ZVxuXHRcdFx0fVxuXHRcdH1cblx0fSxcblx0cHJldmlldzoge1xuXHRcdHBvcnQ6IDMwMDBcblx0fSxcblx0cGx1Z2luczogW1xuXHRcdHN2ZWx0ZWtpdCgpLFxuXHRcdG1vbmFjb0VkaXRvclBsdWdpbi5kZWZhdWx0KHtcblx0XHRcdHB1YmxpY1BhdGg6ICd3b3JrZXJzJyxcblx0XHRcdGxhbmd1YWdlV29ya2VyczogW10sXG5cdFx0XHRjdXN0b21Xb3JrZXJzOiBbXG5cdFx0XHRcdHtcblx0XHRcdFx0XHRsYWJlbDogJ2dyYXBocWwnLFxuXHRcdFx0XHRcdGVudHJ5OiAnbW9uYWNvLWdyYXBocWwvZXNtL2dyYXBocWwud29ya2VyJ1xuXHRcdFx0XHR9XG5cdFx0XHRdXG5cdFx0fSksXG5cdFx0Y2lyY2xlRGVwZW5kZW5jeSh7IGNpcmNsZUltcG9ydFRocm93RXJyOiBmYWxzZSB9KVxuXHRdLFxuXHRkZWZpbmU6IHtcblx0XHRfX3BrZ19fOiB2ZXJzaW9uXG5cdH0sXG5cdG9wdGltaXplRGVwczoge1xuXHRcdGluY2x1ZGU6IFsnaGlnaGxpZ2h0LmpzJywgJ2hpZ2hsaWdodC5qcy9saWIvY29yZSddXG5cdH0sXG5cdHJlc29sdmU6IHtcblx0XHRhbGlhczoge1xuXHRcdFx0cGF0aDogJ3BhdGgtYnJvd3NlcmlmeSdcblx0XHR9LFxuXHRcdGRlZHVwZTogWydtb25hY28tZWRpdG9yJywgJ3ZzY29kZSddXG5cdH0sXG5cdGFzc2V0c0luY2x1ZGU6IFsnKiovKi53YXNtJ11cbn1cblxuZXhwb3J0IGRlZmF1bHQgY29uZmlnXG4iXSwKICAibWFwcGluZ3MiOiAiO0FBQW9QLFNBQVMsaUJBQWlCO0FBQzlRLFNBQVMsb0JBQW9CO0FBQzdCLFNBQVMscUJBQXFCO0FBQzlCLE9BQU8sd0JBQXdCO0FBQy9CLE9BQU8sc0JBQXNCO0FBSnVILElBQU0sMkNBQTJDO0FBTXJNLElBQU0sT0FBTyxjQUFjLElBQUksSUFBSSxnQkFBZ0Isd0NBQWUsQ0FBQztBQUNuRSxJQUFNLE9BQU8sYUFBYSxNQUFNLE1BQU07QUFDdEMsSUFBTSxVQUFVLEtBQUssTUFBTSxJQUFJO0FBRy9CLElBQU0sU0FBUztBQUFBLEVBQ2QsUUFBUTtBQUFBLElBQ1AsTUFBTTtBQUFBLElBQ04sT0FBTztBQUFBLE1BQ04sWUFBWTtBQUFBLFFBQ1gsUUFBUSxRQUFRLElBQUksVUFBVTtBQUFBLFFBQzlCLGNBQWM7QUFBQSxRQUNkLHFCQUFxQjtBQUFBLE1BQ3RCO0FBQUEsTUFDQSxXQUFXO0FBQUEsUUFDVixRQUFRLFFBQVEsSUFBSSxjQUFjO0FBQUEsUUFDbEMsY0FBYztBQUFBLFFBQ2QsSUFBSTtBQUFBLE1BQ0w7QUFBQSxNQUNBLGNBQWM7QUFBQSxRQUNiLFFBQVEsUUFBUSxJQUFJLGFBQWE7QUFBQSxRQUNqQyxjQUFjO0FBQUEsUUFDZCxJQUFJO0FBQUEsTUFDTDtBQUFBLElBQ0Q7QUFBQSxFQUNEO0FBQUEsRUFDQSxTQUFTO0FBQUEsSUFDUixNQUFNO0FBQUEsRUFDUDtBQUFBLEVBQ0EsU0FBUztBQUFBLElBQ1IsVUFBVTtBQUFBLElBQ1YsbUJBQW1CLFFBQVE7QUFBQSxNQUMxQixZQUFZO0FBQUEsTUFDWixpQkFBaUIsQ0FBQztBQUFBLE1BQ2xCLGVBQWU7QUFBQSxRQUNkO0FBQUEsVUFDQyxPQUFPO0FBQUEsVUFDUCxPQUFPO0FBQUEsUUFDUjtBQUFBLE1BQ0Q7QUFBQSxJQUNELENBQUM7QUFBQSxJQUNELGlCQUFpQixFQUFFLHNCQUFzQixNQUFNLENBQUM7QUFBQSxFQUNqRDtBQUFBLEVBQ0EsUUFBUTtBQUFBLElBQ1AsU0FBUztBQUFBLEVBQ1Y7QUFBQSxFQUNBLGNBQWM7QUFBQSxJQUNiLFNBQVMsQ0FBQyxnQkFBZ0IsdUJBQXVCO0FBQUEsRUFDbEQ7QUFBQSxFQUNBLFNBQVM7QUFBQSxJQUNSLE9BQU87QUFBQSxNQUNOLE1BQU07QUFBQSxJQUNQO0FBQUEsSUFDQSxRQUFRLENBQUMsaUJBQWlCLFFBQVE7QUFBQSxFQUNuQztBQUFBLEVBQ0EsZUFBZSxDQUFDLFdBQVc7QUFDNUI7QUFFQSxJQUFPLHNCQUFROyIsCiAgIm5hbWVzIjogW10KfQo=
|
||||
Reference in New Issue
Block a user