feat(editor): Add SQL editor support (#5517)

This commit is contained in:
Jan Oberhauser 2023-04-25 18:18:27 +02:00 committed by GitHub
parent f9b11c73b9
commit 70aaf24784
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 163 additions and 5 deletions

View file

@ -30,6 +30,7 @@
"@codemirror/commands": "^6.1.0",
"@codemirror/lang-javascript": "^6.1.2",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-sql": "^6.4.1",
"@codemirror/language": "^6.2.1",
"@codemirror/lint": "^6.0.0",
"@codemirror/state": "^6.1.4",

View file

@ -103,6 +103,14 @@
@valueChanged="valueChangedDebounced"
/>
<sql-editor
v-else-if="editorType === 'sqlEditor'"
:query="node.parameters.query"
:dialect="getArgument('sqlDialect')"
:isReadOnly="isReadOnly"
@valueChanged="valueChangedDebounced"
/>
<div
v-else-if="editorType"
class="readonly-code clickable ph-no-capture"
@ -367,6 +375,7 @@ import ExpressionParameterInput from '@/components/ExpressionParameterInput.vue'
import TextEdit from '@/components/TextEdit.vue';
import CodeNodeEditor from '@/components/CodeNodeEditor/CodeNodeEditor.vue';
import HtmlEditor from '@/components/HtmlEditor/HtmlEditor.vue';
import SqlEditor from '@/components/SqlEditor/SqlEditor.vue';
import { externalHooks } from '@/mixins/externalHooks';
import { nodeHelpers } from '@/mixins/nodeHelpers';
import { showMessage } from '@/mixins/showMessage';
@ -374,8 +383,7 @@ import { workflowHelpers } from '@/mixins/workflowHelpers';
import { hasExpressionMapping, isValueExpression, isResourceLocatorValue } from '@/utils';
import mixins from 'vue-typed-mixins';
import { CUSTOM_API_CALL_KEY, HTML_NODE_TYPE } from '@/constants';
import { CODE_NODE_TYPE } from '@/constants';
import { CODE_NODE_TYPE, CUSTOM_API_CALL_KEY, HTML_NODE_TYPE } from '@/constants';
import type { PropType } from 'vue';
import { debounceHelper } from '@/mixins/debounce';
import { mapStores } from 'pinia';
@ -398,6 +406,7 @@ export default mixins(
components: {
CodeNodeEditor,
HtmlEditor,
SqlEditor,
ExpressionEdit,
ExpressionParameterInput,
NodeCredentials,

View file

@ -0,0 +1,95 @@
<template>
<div ref="sqlEditor" class="ph-no-capture"></div>
</template>
<script lang="ts">
import type { PropType } from 'vue';
import { defineComponent } from 'vue';
import { autocompletion } from '@codemirror/autocomplete';
import { indentWithTab, history, redo } from '@codemirror/commands';
import { foldGutter, indentOnInput } from '@codemirror/language';
import { lintGutter } from '@codemirror/lint';
import type { Extension } from '@codemirror/state';
import { EditorState } from '@codemirror/state';
import type { ViewUpdate } from '@codemirror/view';
import {
dropCursor,
EditorView,
highlightActiveLine,
highlightActiveLineGutter,
keymap,
lineNumbers,
} from '@codemirror/view';
import { MSSQL, MySQL, PostgreSQL, sql, StandardSQL } from '@codemirror/lang-sql';
import type { SQLDialect } from 'n8n-workflow';
import { codeNodeEditorTheme } from '../CodeNodeEditor/theme';
const SQL_DIALECTS = {
standard: StandardSQL,
mssql: MSSQL,
mysql: MySQL,
postgres: PostgreSQL,
} as const;
export default defineComponent({
name: 'sql-editor',
props: {
query: {
type: String,
required: true,
},
dialect: {
type: String as PropType<SQLDialect>,
default: 'standard',
},
isReadOnly: {
type: Boolean,
default: false,
},
},
data() {
return {
editor: {} as EditorView,
};
},
computed: {
doc(): string {
return this.editor.state.doc.toString();
},
},
mounted() {
const dialect = SQL_DIALECTS[this.dialect as SQLDialect] ?? SQL_DIALECTS.standard;
const extensions: Extension[] = [
sql({ dialect, upperCaseKeywords: true }),
codeNodeEditorTheme({ maxHeight: false }),
lineNumbers(),
EditorView.lineWrapping,
lintGutter(),
EditorState.readOnly.of(this.isReadOnly),
];
if (this.isReadOnly) {
extensions.push(EditorView.editable.of(this.isReadOnly));
} else {
extensions.push(
history(),
keymap.of([indentWithTab, { key: 'Mod-Shift-z', run: redo }]),
autocompletion(),
indentOnInput(),
highlightActiveLine(),
highlightActiveLineGutter(),
foldGutter(),
dropCursor(),
EditorView.updateListener.of((viewUpdate: ViewUpdate) => {
if (!viewUpdate.docChanged) return;
this.$emit('valueChanged', this.doc);
}),
);
}
const state = EditorState.create({ doc: this.query, extensions });
this.editor = new EditorView({ parent: this.$refs.sqlEditor as HTMLDivElement, state });
},
});
</script>

View file

@ -73,6 +73,10 @@ export class CrateDb implements INodeType {
displayName: 'Query',
name: 'query',
type: 'string',
typeOptions: {
editor: 'sqlEditor',
sqlDialect: 'postgres',
},
displayOptions: {
show: {
operation: ['executeQuery'],

View file

@ -14,6 +14,9 @@ const properties: INodeProperties[] = [
displayName: 'SQL Query',
name: 'sqlQuery',
type: 'string',
typeOptions: {
editor: 'sqlEditor',
},
displayOptions: {
hide: {
'/options.useLegacySql': [true],
@ -28,6 +31,9 @@ const properties: INodeProperties[] = [
displayName: 'SQL Query',
name: 'sqlQuery',
type: 'string',
typeOptions: {
editor: 'sqlEditor',
},
displayOptions: {
show: {
'/options.useLegacySql': [true],

View file

@ -90,6 +90,10 @@ export class MicrosoftSql implements INodeType {
displayName: 'Query',
name: 'query',
type: 'string',
typeOptions: {
editor: 'sqlEditor',
sqlDialect: 'mssql',
},
displayOptions: {
show: {
operation: ['executeQuery'],

View file

@ -72,6 +72,10 @@ const versionDescription: INodeTypeDescription = {
displayName: 'Query',
name: 'query',
type: 'string',
typeOptions: {
editor: 'sqlEditor',
sqlDialect: 'mysql',
},
displayOptions: {
show: {
operation: ['executeQuery'],

View file

@ -21,7 +21,8 @@ const properties: INodeProperties[] = [
description:
"The SQL query to execute. You can use n8n expressions and $1, $2, $3, etc to refer to the 'Query Parameters' set in options below.",
typeOptions: {
rows: 3,
editor: 'sqlEditor',
sqlDialect: 'mysql',
},
hint: 'Prefer using query parameters over n8n expressions to avoid SQL injection attacks',
},

View file

@ -71,6 +71,10 @@ const versionDescription: INodeTypeDescription = {
displayName: 'Query',
name: 'query',
type: 'string',
typeOptions: {
editor: 'sqlEditor',
sqlDialect: 'postgres',
},
displayOptions: {
show: {
operation: ['executeQuery'],

View file

@ -21,7 +21,8 @@ const properties: INodeProperties[] = [
description:
"The SQL query to execute. You can use n8n expressions and $1, $2, $3, etc to refer to the 'Query Parameters' set in options below.",
typeOptions: {
rows: 3,
editor: 'sqlEditor',
sqlDialect: 'postgres',
},
hint: 'Prefer using query parameters over n8n expressions to avoid SQL injection attacks',
},

View file

@ -60,6 +60,10 @@ export class QuestDb implements INodeType {
displayName: 'Query',
name: 'query',
type: 'string',
typeOptions: {
editor: 'sqlEditor',
sqlDialect: 'postgres',
},
displayOptions: {
show: {
operation: ['executeQuery'],

View file

@ -65,6 +65,9 @@ export class Snowflake implements INodeType {
displayName: 'Query',
name: 'query',
type: 'string',
typeOptions: {
editor: 'sqlEditor',
},
displayOptions: {
show: {
operation: ['executeQuery'],

View file

@ -65,6 +65,10 @@ export class TimescaleDb implements INodeType {
displayName: 'Query',
name: 'query',
type: 'string',
typeOptions: {
editor: 'sqlEditor',
sqlDialect: 'postgres',
},
displayOptions: {
show: {
operation: ['executeQuery'],

View file

@ -1019,8 +1019,9 @@ export type NodePropertyTypes =
export type CodeAutocompleteTypes = 'function' | 'functionItem';
export type EditorType = 'code' | 'codeNodeEditor' | 'htmlEditor' | 'json';
export type EditorType = 'code' | 'codeNodeEditor' | 'htmlEditor' | 'sqlEditor' | 'json';
export type CodeNodeEditorLanguage = 'javaScript' | 'json'; //| 'python' | 'sql';
export type SQLDialect = 'mssql' | 'mysql' | 'postgres';
export interface ILoadOptions {
routing?: {
@ -1035,6 +1036,7 @@ export interface INodePropertyTypeOptions {
codeAutocomplete?: CodeAutocompleteTypes; // Supported by: string
editor?: EditorType; // Supported by: string
editorLanguage?: CodeNodeEditorLanguage; // Supported by: string in combination with editor: codeNodeEditor
sqlDialect?: SQLDialect; // Supported by: sqlEditor
loadOptionsDependsOn?: string[]; // Supported by: options
loadOptionsMethod?: string; // Supported by: options
loadOptions?: ILoadOptions; // Supported by: options

View file

@ -844,6 +844,9 @@ importers:
'@codemirror/lang-json':
specifier: ^6.0.1
version: 6.0.1
'@codemirror/lang-sql':
specifier: ^6.4.1
version: 6.4.1(@codemirror/view@6.5.1)(@lezer/common@1.0.1)
'@codemirror/language':
specifier: ^6.2.1
version: 6.2.1
@ -3367,6 +3370,19 @@ packages:
'@lezer/json': 1.0.0
dev: false
/@codemirror/lang-sql@6.4.1(@codemirror/view@6.5.1)(@lezer/common@1.0.1):
resolution: {integrity: sha512-PFB56L+A0WGY35uRya+Trt5g19V9k2V9X3c55xoFW4RgiATr/yLqWsbbnEsdxuMn5tLpuikp7Kmj9smRsqBXAg==}
dependencies:
'@codemirror/autocomplete': 6.4.0(@codemirror/language@6.2.1)(@codemirror/state@6.1.4)(@codemirror/view@6.5.1)(@lezer/common@1.0.1)
'@codemirror/language': 6.2.1
'@codemirror/state': 6.1.4
'@lezer/highlight': 1.1.1
'@lezer/lr': 1.2.3
transitivePeerDependencies:
- '@codemirror/view'
- '@lezer/common'
dev: false
/@codemirror/language@6.2.1:
resolution: {integrity: sha512-MC3svxuvIj0MRpFlGHxLS6vPyIdbTr2KKPEW46kCoCXw2ktb4NTkpkPBI/lSP/FoNXLCBJ0mrnUi1OoZxtpW1Q==}
dependencies: