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:
Faton Ramadani
2024-03-05 08:52:20 +01:00
committed by GitHub
parent 5407265419
commit ca6311d8cd
24 changed files with 1550 additions and 714 deletions
+4 -2
View File
@@ -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)
@@ -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,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 ?? []
}
@@ -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: {
@@ -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' }>
+13 -9
View File
@@ -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',
+1 -1
View File
@@ -151,7 +151,7 @@ export async function inferArgs(
await tick()
}
function argSigToJsonSchemaType(
export function argSigToJsonSchemaType(
t:
| string
| { resource: string | null }
+5 -2
View File
@@ -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=