From 212c9d76e5608bc7d99c4231d7aad70d960fe2e1 Mon Sep 17 00:00:00 2001 From: Faton Ramadani Date: Tue, 12 Mar 2024 17:42:32 +0100 Subject: [PATCH] feat(frontend): DB Studio improvements (#3389) * fix(frontend): wip * fix(frontend): validate column def * fix(frontend): add column definition loading sate * fix(frontend): only reload columns if static * fix(frontend): improve validation * fix(frontend): improve reactivity * fix(frontend): fix colum defs sync * fix(frontend): fix colum defs sync + insert * fix(frontend): fix insert * fix(frontend): fix * fix(frontend): fix insert * fix(frontend): fix insert * fix(frontend): fix hideSearch + hideInsert * fix(frontend): fix early return * fix(frontend): fix delete + policy * fix(frontend): fix delete * fix(frontend): restrict resource + table to static only --- .../display/dbtable/AppDbExplorer.svelte | 139 +++++++++++++----- .../display/dbtable/DeleteRow.svelte | 7 +- .../display/dbtable/InsertRow.svelte | 3 +- .../display/dbtable/queries/delete.ts | 12 +- .../display/dbtable/queries/insert.ts | 76 ++++++++-- .../table/AppAggridExplorerTable.svelte | 27 +++- .../apps/editor/AppEditorHeader.svelte | 9 +- .../apps/editor/component/components.ts | 22 ++- .../ArrayStaticInputEditor.svelte | 12 +- .../OneOfInputSpecsEditor.svelte | 1 + 10 files changed, 235 insertions(+), 73 deletions(-) diff --git a/frontend/src/lib/components/apps/components/display/dbtable/AppDbExplorer.svelte b/frontend/src/lib/components/apps/components/display/dbtable/AppDbExplorer.svelte index d634942487..5b8b1cfb4b 100644 --- a/frontend/src/lib/components/apps/components/display/dbtable/AppDbExplorer.svelte +++ b/frontend/src/lib/components/apps/components/display/dbtable/AppDbExplorer.svelte @@ -57,6 +57,10 @@ return } + if (lastTable && !table) { + lastTable = undefined + } + const gridItem = findGridItem($app, id) if (!gridItem) { @@ -64,7 +68,7 @@ } // @ts-ignore - gridItem.data.configuration.columnDefs = { value: [], type: 'static' } + gridItem.data.configuration.columnDefs = { value: [], type: 'static', loading: false } $app = { ...$app @@ -319,6 +323,32 @@ let lastTable: string | undefined = undefined let timeout: NodeJS.Timeout | undefined = undefined + function isSubset(subset: Record, superset: Record) { + return Object.keys(subset).every((key) => { + return superset[key] === subset[key] + }) + } + + function shouldReturnEarly(subset: Record, superset: Record): boolean { + const subsetKeys = Object.keys(subset) + const supersetKeys = Object.keys(superset) + + if (supersetKeys.length === 0) return false + + if (subsetKeys.length !== supersetKeys.length) { + return false + } + + if ( + JSON.stringify(supersetKeys.sort()) === JSON.stringify(subsetKeys.sort()) && + !subsetKeys.every((key) => isSubset(subset[key], superset[key])) + ) { + return false + } + + return true + } + async function listColumnsIfAvailable() { const selected = resolvedConfig.type.selected let table = resolvedConfig.type.configuration?.[resolvedConfig.type.selected]?.table @@ -327,6 +357,18 @@ lastTable = table + const gridItem = findGridItem($app, id) + if (!gridItem) return + + let columnDefs = gridItem.data.configuration.columnDefs as StaticInput + + if (columnDefs.type !== 'static') return + + //@ts-ignore + gridItem.data.configuration.columnDefs.loading = true + gridItem.data = gridItem.data + $app = $app + let tableMetadata = await loadTableMetaData( resolvedConfig.type.configuration[selected].resource, $workspaceStore, @@ -336,23 +378,21 @@ if (!tableMetadata) return - const gridItem = findGridItem($app, id) - if (!gridItem) return - - let columnDefs = gridItem.data.configuration.columnDefs as StaticInput - let old: TableMetadata = (columnDefs?.value as TableMetadata) ?? [] if (!Array.isArray(old)) { console.log('old is not an array RESET') old = [] } - // console.log('OLD', old) - // console.log(tableMetadata) + 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)) { + if (shouldReturnEarly(newMap, oldMap)) { + //@ts-ignore + gridItem.data.configuration.columnDefs.loading = false + gridItem.data = gridItem.data + + $app = $app return } @@ -375,7 +415,21 @@ ncols = ncols.map((x) => { let o = {} Object.keys(x).forEach((k) => { - o[k.toLowerCase()] = x[k] + if ( + [ + 'field', + 'datatype', + 'defaultvalue', + 'isprimarykey', + 'isidentity', + 'isnullable', + 'isenum' + ].includes(k.toLocaleLowerCase()) + ) { + o[k.toLowerCase()] = x[k] + } else { + o[k] = x[k] + } }) return o }) @@ -383,7 +437,7 @@ state = undefined //@ts-ignore - gridItem.data.configuration.columnDefs = { value: ncols, type: 'static' } + gridItem.data.configuration.columnDefs = { value: ncols, type: 'static', loading: false } gridItem.data = gridItem.data $app = $app @@ -415,7 +469,8 @@ id: 'dbexplorer-count-' + id, next: (value) => { if (value?.error) { - sendUserToast(value.error, true) + const message = value?.error?.message ?? value?.error + sendUserToast(message, true) return } @@ -445,6 +500,7 @@ async function insert() { try { const selected = resolvedConfig.type.selected + await insertRowRunnable?.insertRow( resolvedConfig.type.configuration[selected].resource, $workspaceStore, @@ -471,21 +527,22 @@ function onDelete(e) { const data = { ...e.detail } delete data['__index'] - let primaryColumns = getPrimaryKeys(resolvedConfig.columnDefs) - let getPrimaryKeysresolvedConfig = resolvedConfig.columnDefs?.filter((x) => - primaryColumns.includes(x.field) - ) + const selected = resolvedConfig.type.selected + deleteRow?.triggerDelete( resolvedConfig.type.configuration[selected].resource, resolvedConfig.type.configuration[selected].table ?? 'unknown', - getPrimaryKeysresolvedConfig, + resolvedConfig.columnDefs, data, selected ) } let refreshCount = 0 + + $: hideSearch = resolvedConfig.hideSearch as boolean + $: hideInsert = resolvedConfig.hideInsert as boolean {#each Object.keys(components['dbexplorercomponent'].initialData.configuration) as key (key)} @@ -543,25 +600,31 @@ {outputs} >
-
- - -
+ {#if !(hideSearch === true && hideInsert === true)} +
+ {#if hideSearch !== true} + + {/if} + {#if hideInsert !== true} + + {/if} +
+ {/if} {#if resolvedConfig.type.configuration?.[resolvedConfig?.type?.selected]?.resource && resolvedConfig.type.configuration?.[resolvedConfig?.type?.selected]?.table} @@ -576,7 +639,7 @@ {customCss} {outputs} allowDelete={resolvedConfig.allowDelete ?? false} - containerHeight={componentContainerHeight - buttonContainerHeight} + containerHeight={componentContainerHeight - (buttonContainerHeight ?? 0)} on:update={onUpdate} on:delete={onDelete} /> diff --git a/frontend/src/lib/components/apps/components/display/dbtable/DeleteRow.svelte b/frontend/src/lib/components/apps/components/display/dbtable/DeleteRow.svelte index 66110f22c2..f2093b8506 100644 --- a/frontend/src/lib/components/apps/components/display/dbtable/DeleteRow.svelte +++ b/frontend/src/lib/components/apps/components/display/dbtable/DeleteRow.svelte @@ -5,7 +5,7 @@ import type RunnableComponent from '../../helpers/RunnableComponent.svelte' import RunnableWrapper from '../../helpers/RunnableWrapper.svelte' import { initOutput } from '../../../editor/appUtils' - import { type ColumnDef, type DbType } from './utils' + import { getPrimaryKeys, type ColumnDef, type DbType } from './utils' import { sendUserToast } from '$lib/toast' import { getDeleteInput } from './queries/delete' @@ -29,11 +29,12 @@ export async function triggerDelete( resource: string, table: string, - columns: ColumnDef[], + allColumns: ColumnDef[], data: Record, dbType: DbType ) { - // const datatype = tableMetaData?.find((column) => column.isprimarykey)?.datatype + let primaryColumns = getPrimaryKeys(allColumns) + let columns = allColumns?.filter((x) => primaryColumns.includes(x.field)) input = getDeleteInput(resource, table, columns, dbType) diff --git a/frontend/src/lib/components/apps/components/display/dbtable/InsertRow.svelte b/frontend/src/lib/components/apps/components/display/dbtable/InsertRow.svelte index a3a21c3aa8..91e32a8e30 100644 --- a/frontend/src/lib/components/apps/components/display/dbtable/InsertRow.svelte +++ b/frontend/src/lib/components/apps/components/display/dbtable/InsertRow.svelte @@ -100,7 +100,6 @@ const args = await parseSQLArgs(insertCode, dbType) fields.forEach((field) => { - console.log(field) const schemaProperty: SchemaProperty = { type: 'string' } @@ -111,7 +110,7 @@ } if (field.defaultValue) { - if (schemaProperty.type === 'number') { + if (schemaProperty.type === 'number' || schemaProperty.type === 'integer') { schemaProperty.default = field.defaultValue ? Number(field.defaultValue) : undefined } else if (schemaProperty.type === 'boolean') { schemaProperty.default = field.defaultValue?.toLocaleLowerCase() === 'true' diff --git a/frontend/src/lib/components/apps/components/display/dbtable/queries/delete.ts b/frontend/src/lib/components/apps/components/display/dbtable/queries/delete.ts index 92311acd5b..78f7325804 100644 --- a/frontend/src/lib/components/apps/components/display/dbtable/queries/delete.ts +++ b/frontend/src/lib/components/apps/components/display/dbtable/queries/delete.ts @@ -1,7 +1,7 @@ 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) { +function deleteWithAllValues(table: string, columns: ColumnDef[], dbType: DbType) { let query = buildParameters(columns, dbType) switch (dbType) { @@ -48,11 +48,11 @@ export function getDeleteInput( return undefined } - const updateRunnable: RunnableByName = { + const deleteRunnable: RunnableByName = { name: 'AppDbExplorer', type: 'runnableByName', inlineScript: { - content: updateWithAllValues(table, columns, dbType), + content: deleteWithAllValues(table, columns, dbType), language: getLanguageByResourceType(dbType), schema: { $schema: 'https://json-schema.org/draft/2020-12/schema', @@ -63,8 +63,8 @@ export function getDeleteInput( } } - const updateQuery: AppInput = { - runnable: updateRunnable, + const deleteQuery: AppInput = { + runnable: deleteRunnable, fields: { database: { type: 'static', @@ -77,5 +77,5 @@ export function getDeleteInput( fieldType: 'object' } - return updateQuery + return deleteQuery } diff --git a/frontend/src/lib/components/apps/components/display/dbtable/queries/insert.ts b/frontend/src/lib/components/apps/components/display/dbtable/queries/insert.ts index f7ddecb7a2..40ae8ef65d 100644 --- a/frontend/src/lib/components/apps/components/display/dbtable/queries/insert.ts +++ b/frontend/src/lib/components/apps/components/display/dbtable/queries/insert.ts @@ -1,13 +1,27 @@ import type { AppInput } from '$lib/components/apps/inputType' -import { buildParameters, type DbType } from '../utils' +import { buildParameters, ColumnIdentity, type DbType } from '../utils' import { getLanguageByResourceType, type ColumnDef } from '../utils' +function mapDataTypeToDbType(dataType: string): string { + if (dataType === 'integer') { + return 'int' + } + + if (dataType === 'boolean') { + return 'bool' + } + + return dataType +} + 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(', ') + return columns + .map((c, i) => `$${startIndex + i}::${mapDataTypeToDbType(c.datatype)}`) + .join(', ') case 'ms_sql_server': return columns.map((c, i) => `@p${startIndex + i}`).join(', ') case 'snowflake': @@ -23,36 +37,72 @@ function formatColumnNames(columns: ColumnDef[]): string { return columns.map((c) => c.field).join(', ') } +function getUserDefaultValue(column: ColumnDef) { + if (column.defaultValueNull) { + return 'NULL' + } else if (column.defaultUserValue) { + return typeof column.defaultUserValue === 'string' + ? `'${column.defaultUserValue}'` + : column.defaultUserValue + } +} + 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 + const userDefaultValue = getUserDefaultValue(c) + if (c.overrideDefaultValue === true) { + return userDefaultValue } + + return userDefaultValue ?? c.defaultvalue }) .join(', ') return defaultValues } +function shouldOmitColumnInInsert(column: ColumnDef) { + if (!column.hideInsert || column.isidentity === ColumnIdentity.Always) { + return true + } + + const userDefaultValue = + (column.defaultUserValue !== undefined && column.defaultUserValue !== '') || + column.defaultValueNull === true + const dbDefaultValue = Boolean(column.defaultvalue) + + if (column.isnullable === 'NO') { + if (!userDefaultValue && !dbDefaultValue && column.isidentity === ColumnIdentity.No) { + throw new Error(`Column ${column.field} is not nullable and has no default value`) + } + + if (!userDefaultValue && !dbDefaultValue) { + // Should be omitted if it's an identity column and we have no default value + return column.isidentity !== ColumnIdentity.No + } + + // Should be omitted if the user had not provided a default value and the database has a default value + return !userDefaultValue && dbDefaultValue + } else if (column.isnullable === 'YES') { + return !userDefaultValue + } + + return false +} + 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 columnsDefault = columns.filter((c) => !shouldOmitColumnInInsert(c)) const allInsertColumns = columnsInsert.concat(columnsDefault) + let query = buildParameters(columnsInsert, dbType) query += '\n' - const shouldInsertComma = columnsDefault.length > 0 && columnsInsert.length > 0 + const shouldInsertComma = columnsDefault.length > 0 const columnNames = formatColumnNames(allInsertColumns) const insertValues = formatInsertValues(columnsInsert, dbType) const defaultValues = formatDefaultValues(columnsDefault) diff --git a/frontend/src/lib/components/apps/components/display/table/AppAggridExplorerTable.svelte b/frontend/src/lib/components/apps/components/display/table/AppAggridExplorerTable.svelte index 053cbeafa0..c00c5a17f4 100644 --- a/frontend/src/lib/components/apps/components/display/table/AppAggridExplorerTable.svelte +++ b/frontend/src/lib/components/apps/components/display/table/AppAggridExplorerTable.svelte @@ -1,6 +1,6 @@