mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-09 22:24:05 -08:00
feat(editor): SQL editor overhaul (#6282)
* Draft setup * ⚡ Implemented expression evaluation in Postgres node, minor SQL editor UI improvements, minor refacring * ⚡ Added initial version of expression preview for SQL editor * ⚡ Linking npm package for codemirror sql grammar instead of a local file * ⚡ Moving expression editor wrapper elements to the component * ⚡ Using expression preview in SQL editor * Use SQL parser skipping whitespace * ✨ Added support for custom skipped segments specification * ✨ Fixing highlight problems with dots and expressions that resolve to zero * 👕 Fixing linting error * ✨ Added current item support * ⚡ Added expression support to more nodes with sql editor * ✨ Added expression support for other nodes * ✨ Implemented different SQL dialect support * 🐛 Fixing hard-coded parameter names for editors * ✨ Fixing preview for nested queries, updating query when input data changes, adding keyboard shortcut to toggle comments * ✨ Adding a custom automcomplete notice for different editors * ⚡ Updating SQL autocomplete notice * ✅ Added unit tests for SQL editor * ⚡ Using latest grammar * 🐛 Fixing code node editor rendering * 💄 SQL preview dropdown matches editor width. Removing unnecessary css * ⚡ Addressing PR review feedback * 👌 Addressing PR review feedback pt2 * 👌 Added path alias for utils in nodes-base package * 👌 Addressing more PR review feedback * ✅ Adding tests for `getResolvables` utility function * ⚡Fixing lodash imports * 👌 Better focus handling, adding more plugins to the editor, other minor imrovements * ⚡ Not showing SQL autocomplete suggestions inside expressions * ⚡ Using npm package for sql grammar * ⚡ Removing autocomplete notice, adding line highlight on syntax error * 👌 Addressing code review feedback --------- Co-authored-by: Milorad Filipovic <milorad@n8n.io>
This commit is contained in:
parent
d431117c9e
commit
beedfb609c
|
@ -31,7 +31,6 @@
|
|||
"@codemirror/lang-javascript": "^6.1.2",
|
||||
"@codemirror/lang-json": "^6.0.1",
|
||||
"@codemirror/lang-python": "^6.1.2",
|
||||
"@codemirror/lang-sql": "^6.4.1",
|
||||
"@codemirror/language": "^6.2.1",
|
||||
"@codemirror/lint": "^6.0.0",
|
||||
"@codemirror/state": "^6.1.4",
|
||||
|
@ -46,6 +45,7 @@
|
|||
"@jsplumb/connector-bezier": "^5.13.2",
|
||||
"@jsplumb/core": "^5.13.2",
|
||||
"@jsplumb/util": "^5.13.2",
|
||||
"@n8n/codemirror-lang-sql": "^1.0.0",
|
||||
"axios": "^0.21.1",
|
||||
"codemirror-lang-html-n8n": "^1.0.0",
|
||||
"codemirror-lang-n8n-expression": "^0.2.0",
|
||||
|
|
|
@ -31,9 +31,10 @@ const cssStyleDeclaration = getComputedStyle(document.documentElement);
|
|||
|
||||
interface ThemeSettings {
|
||||
isReadOnly?: boolean;
|
||||
customMaxHeight?: string;
|
||||
}
|
||||
|
||||
export const codeNodeEditorTheme = ({ isReadOnly }: ThemeSettings) => [
|
||||
export const codeNodeEditorTheme = ({ isReadOnly, customMaxHeight }: ThemeSettings) => [
|
||||
EditorView.theme({
|
||||
'&': {
|
||||
'font-size': BASE_STYLING.fontSize,
|
||||
|
@ -79,7 +80,7 @@ export const codeNodeEditorTheme = ({ isReadOnly }: ThemeSettings) => [
|
|||
},
|
||||
'.cm-scroller': {
|
||||
overflow: 'auto',
|
||||
maxHeight: '100%',
|
||||
maxHeight: customMaxHeight ?? '100%',
|
||||
...(isReadOnly ? {} : { minHeight: '10em' }),
|
||||
},
|
||||
'.cm-diagnosticAction': {
|
||||
|
|
|
@ -31,36 +31,13 @@
|
|||
/>
|
||||
</div>
|
||||
|
||||
<div :class="isFocused ? $style.dropdown : $style.hidden">
|
||||
<n8n-text size="small" compact :class="$style.header">
|
||||
{{ $locale.baseText('parameterInput.resultForItem') }} {{ hoveringItemNumber }}
|
||||
</n8n-text>
|
||||
<n8n-text :class="$style.body">
|
||||
<InlineExpressionEditorOutput
|
||||
:value="value"
|
||||
:isReadOnly="isReadOnly"
|
||||
:segments="segments"
|
||||
/>
|
||||
</n8n-text>
|
||||
<div :class="$style.footer">
|
||||
<n8n-text size="small" compact>
|
||||
{{ $locale.baseText('parameterInput.anythingInside') }}
|
||||
</n8n-text>
|
||||
<div :class="$style['expression-syntax-example']" v-text="`{{ }}`"></div>
|
||||
<n8n-text size="small" compact>
|
||||
{{ $locale.baseText('parameterInput.isJavaScript') }}
|
||||
</n8n-text>
|
||||
<n8n-link
|
||||
:class="$style['learn-more']"
|
||||
size="small"
|
||||
underline
|
||||
theme="text"
|
||||
:to="expressionsDocsUrl"
|
||||
>
|
||||
{{ $locale.baseText('parameterInput.learnMore') }}
|
||||
</n8n-link>
|
||||
</div>
|
||||
</div>
|
||||
<InlineExpressionEditorOutput
|
||||
:segments="segments"
|
||||
:value="value"
|
||||
:isReadOnly="isReadOnly"
|
||||
:visible="isFocused"
|
||||
:hoveringItemNumber="hoveringItemNumber"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -74,7 +51,6 @@ import InlineExpressionEditorInput from '@/components/InlineExpressionEditor/Inl
|
|||
import InlineExpressionEditorOutput from '@/components/InlineExpressionEditor/InlineExpressionEditorOutput.vue';
|
||||
import ExpressionFunctionIcon from '@/components/ExpressionFunctionIcon.vue';
|
||||
import { createExpressionTelemetryPayload } from '@/utils/telemetryUtils';
|
||||
import { EXPRESSIONS_DOCS_URL } from '@/constants';
|
||||
|
||||
import type { Segment } from '@/types/expressions';
|
||||
import type { TargetItem } from '@/Interface';
|
||||
|
@ -92,7 +68,6 @@ export default defineComponent({
|
|||
return {
|
||||
isFocused: false,
|
||||
segments: [] as Segment[],
|
||||
expressionsDocsUrl: EXPRESSIONS_DOCS_URL,
|
||||
};
|
||||
},
|
||||
props: {
|
||||
|
@ -114,14 +89,10 @@ export default defineComponent({
|
|||
computed: {
|
||||
...mapStores(useNDVStore, useWorkflowsStore),
|
||||
hoveringItemNumber(): number {
|
||||
return (this.hoveringItem?.itemIndex ?? 0) + 1;
|
||||
return this.ndvStore.hoveringItemNumber;
|
||||
},
|
||||
hoveringItem(): TargetItem | null {
|
||||
if (this.ndvStore.isInputParentOfActiveNode) {
|
||||
return this.ndvStore.hoveringItem;
|
||||
}
|
||||
|
||||
return null;
|
||||
return this.ndvStore.getHoveringItem;
|
||||
},
|
||||
isDragging(): boolean {
|
||||
return this.ndvStore.isDraggableDragging;
|
||||
|
@ -241,64 +212,4 @@ export default defineComponent({
|
|||
border-bottom-right-radius: 0;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
z-index: 2; // cover tooltips
|
||||
background: white;
|
||||
border: var(--border-base);
|
||||
border-top: none;
|
||||
width: 100%;
|
||||
box-shadow: 0 2px 6px 0 rgba(#441c17, 0.1);
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
|
||||
.header,
|
||||
.body,
|
||||
.footer {
|
||||
padding: var(--spacing-3xs);
|
||||
}
|
||||
|
||||
.header {
|
||||
color: var(--color-text-dark);
|
||||
font-weight: var(--font-weight-bold);
|
||||
padding-left: var(--spacing-2xs);
|
||||
padding-top: var(--spacing-2xs);
|
||||
}
|
||||
|
||||
.body {
|
||||
padding-top: 0;
|
||||
padding-left: var(--spacing-2xs);
|
||||
color: var(--color-text-dark);
|
||||
}
|
||||
|
||||
.footer {
|
||||
border-top: var(--border-base);
|
||||
padding: var(--spacing-4xs);
|
||||
padding-left: var(--spacing-2xs);
|
||||
padding-top: 0;
|
||||
line-height: var(--font-line-height-regular);
|
||||
color: var(--color-text-base);
|
||||
|
||||
.expression-syntax-example {
|
||||
display: inline-block;
|
||||
font-size: var(--font-size-2xs);
|
||||
height: var(--font-size-m);
|
||||
background-color: #f0f0f0;
|
||||
margin-left: var(--spacing-5xs);
|
||||
margin-right: var(--spacing-5xs);
|
||||
}
|
||||
|
||||
.learn-more {
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,5 +1,30 @@
|
|||
<template>
|
||||
<div ref="root" class="ph-no-capture" data-test-id="inline-expression-editor-output"></div>
|
||||
<div :class="visible ? $style.dropdown : $style.hidden">
|
||||
<n8n-text size="small" compact :class="$style.header">
|
||||
{{ $locale.baseText('parameterInput.resultForItem') }} {{ hoveringItemNumber }}
|
||||
</n8n-text>
|
||||
<n8n-text :class="$style.body">
|
||||
<div ref="root" class="ph-no-capture" data-test-id="inline-expression-editor-output"></div>
|
||||
</n8n-text>
|
||||
<div :class="$style.footer">
|
||||
<n8n-text size="small" compact>
|
||||
{{ $locale.baseText('parameterInput.anythingInside') }}
|
||||
</n8n-text>
|
||||
<div :class="$style['expression-syntax-example']" v-text="`{{ }}`"></div>
|
||||
<n8n-text size="small" compact>
|
||||
{{ $locale.baseText('parameterInput.isJavaScript') }}
|
||||
</n8n-text>
|
||||
<n8n-link
|
||||
:class="$style['learn-more']"
|
||||
size="small"
|
||||
underline
|
||||
theme="text"
|
||||
:to="expressionsDocsUrl"
|
||||
>
|
||||
{{ $locale.baseText('parameterInput.learnMore') }}
|
||||
</n8n-link>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
@ -13,12 +38,29 @@ import { highlighter } from '@/plugins/codemirror/resolvableHighlighter';
|
|||
import { outputTheme } from './theme';
|
||||
|
||||
import type { Plaintext, Resolved, Segment } from '@/types/expressions';
|
||||
import { EXPRESSIONS_DOCS_URL } from '@/constants';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'InlineExpressionEditorOutput',
|
||||
props: {
|
||||
segments: {
|
||||
type: Array as PropType<Segment[]>,
|
||||
required: true,
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
},
|
||||
isReadOnly: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
hoveringItemNumber: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
|
@ -36,6 +78,7 @@ export default defineComponent({
|
|||
data() {
|
||||
return {
|
||||
editor: null as EditorView | null,
|
||||
expressionsDocsUrl: EXPRESSIONS_DOCS_URL,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
|
@ -87,4 +130,64 @@ export default defineComponent({
|
|||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss"></style>
|
||||
<style lang="scss" module>
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
z-index: 2; // cover tooltips
|
||||
background: white;
|
||||
border: var(--border-base);
|
||||
border-top: none;
|
||||
width: 100%;
|
||||
box-shadow: 0 2px 6px 0 rgba(#441c17, 0.1);
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
|
||||
.header,
|
||||
.body,
|
||||
.footer {
|
||||
padding: var(--spacing-3xs);
|
||||
}
|
||||
|
||||
.header {
|
||||
color: var(--color-text-dark);
|
||||
font-weight: var(--font-weight-bold);
|
||||
padding-left: var(--spacing-2xs);
|
||||
padding-top: var(--spacing-2xs);
|
||||
}
|
||||
|
||||
.body {
|
||||
padding-top: 0;
|
||||
padding-left: var(--spacing-2xs);
|
||||
color: var(--color-text-dark);
|
||||
}
|
||||
|
||||
.footer {
|
||||
border-top: var(--border-base);
|
||||
padding: var(--spacing-4xs);
|
||||
padding-left: var(--spacing-2xs);
|
||||
padding-top: 0;
|
||||
line-height: var(--font-line-height-regular);
|
||||
color: var(--color-text-base);
|
||||
|
||||
.expression-syntax-example {
|
||||
display: inline-block;
|
||||
font-size: var(--font-size-2xs);
|
||||
height: var(--font-size-m);
|
||||
background-color: #f0f0f0;
|
||||
margin-left: var(--spacing-5xs);
|
||||
margin-right: var(--spacing-5xs);
|
||||
}
|
||||
|
||||
.learn-more {
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -107,7 +107,7 @@ export default defineComponent({
|
|||
return false;
|
||||
}
|
||||
|
||||
if (this.parameter.typeOptions?.editor === 'codeNodeEditor') {
|
||||
if (['codeNodeEditor', 'sqlEditor'].includes(this.parameter.typeOptions?.editor)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,17 +1,23 @@
|
|||
<template>
|
||||
<div ref="sqlEditor" class="ph-no-capture"></div>
|
||||
<div :class="$style.sqlEditor" v-click-outside="onBlur">
|
||||
<div ref="sqlEditor" data-test-id="sql-editor-container" class="ph-no-capture"></div>
|
||||
<InlineExpressionEditorOutput
|
||||
:segments="segments"
|
||||
:value="query"
|
||||
:isReadOnly="isReadOnly"
|
||||
:visible="isFocused"
|
||||
:hoveringItemNumber="hoveringItemNumber"
|
||||
/>
|
||||
</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 { acceptCompletion, autocompletion, ifNotIn } from '@codemirror/autocomplete';
|
||||
import { indentWithTab, history, redo, toggleComment } from '@codemirror/commands';
|
||||
import { bracketMatching, foldGutter, indentOnInput, LanguageSupport } from '@codemirror/language';
|
||||
import { EditorState } from '@codemirror/state';
|
||||
import type { ViewUpdate } from '@codemirror/view';
|
||||
import type { Extension } from '@codemirror/state';
|
||||
import {
|
||||
dropCursor,
|
||||
EditorView,
|
||||
|
@ -20,76 +26,189 @@ import {
|
|||
keymap,
|
||||
lineNumbers,
|
||||
} from '@codemirror/view';
|
||||
import { MSSQL, MySQL, PostgreSQL, sql, StandardSQL } from '@codemirror/lang-sql';
|
||||
import type { SQLDialect } from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
MSSQL,
|
||||
MySQL,
|
||||
PostgreSQL,
|
||||
StandardSQL,
|
||||
MariaSQL,
|
||||
SQLite,
|
||||
Cassandra,
|
||||
PLSQL,
|
||||
keywordCompletionSource,
|
||||
} from '@n8n/codemirror-lang-sql';
|
||||
import type { SQLDialect as SQLDialectType } from '@n8n/codemirror-lang-sql';
|
||||
import { codeNodeEditorTheme } from '../CodeNodeEditor/theme';
|
||||
import { n8nCompletionSources } from '@/plugins/codemirror/completions/addCompletions';
|
||||
import { expressionInputHandler } from '@/plugins/codemirror/inputHandlers/expression.inputHandler';
|
||||
import { highlighter } from '@/plugins/codemirror/resolvableHighlighter';
|
||||
import { expressionManager } from '@/mixins/expressionManager';
|
||||
import InlineExpressionEditorOutput from '@/components/InlineExpressionEditor/InlineExpressionEditorOutput.vue';
|
||||
import { EXPRESSIONS_DOCS_URL } from '@/constants';
|
||||
import { codeNodeEditorEventBus } from '@/event-bus';
|
||||
|
||||
const SQL_DIALECTS = {
|
||||
standard: StandardSQL,
|
||||
mssql: MSSQL,
|
||||
mysql: MySQL,
|
||||
postgres: PostgreSQL,
|
||||
StandardSQL,
|
||||
PostgreSQL,
|
||||
MySQL,
|
||||
MariaSQL,
|
||||
MSSQL,
|
||||
SQLite,
|
||||
Cassandra,
|
||||
PLSQL,
|
||||
} as const;
|
||||
|
||||
type SQLEditorData = {
|
||||
editor: EditorView | null;
|
||||
isFocused: boolean;
|
||||
skipSegments: string[];
|
||||
expressionsDocsUrl: string;
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: 'sql-editor',
|
||||
components: {
|
||||
InlineExpressionEditorOutput,
|
||||
},
|
||||
mixins: [expressionManager],
|
||||
props: {
|
||||
query: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
dialect: {
|
||||
type: String as PropType<SQLDialect>,
|
||||
default: 'standard',
|
||||
type: String,
|
||||
default: 'StandardSQL',
|
||||
validator: (value: string) => {
|
||||
return Object.keys(SQL_DIALECTS).includes(value);
|
||||
},
|
||||
},
|
||||
isReadOnly: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
data(): SQLEditorData {
|
||||
return {
|
||||
editor: {} as EditorView,
|
||||
editor: null,
|
||||
expressionsDocsUrl: EXPRESSIONS_DOCS_URL,
|
||||
isFocused: false,
|
||||
skipSegments: ['Statement', 'CompositeIdentifier', 'Parens'],
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
'ndvStore.ndvInputData'() {
|
||||
this.editor?.dispatch({
|
||||
changes: {
|
||||
from: 0,
|
||||
to: this.editor.state.doc.length,
|
||||
insert: this.query,
|
||||
},
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
this.editor?.contentDOM.blur();
|
||||
});
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
doc(): string {
|
||||
return this.editor.state.doc.toString();
|
||||
return this.editor?.state.doc.toString() ?? '';
|
||||
},
|
||||
hoveringItemNumber(): number {
|
||||
return this.ndvStore.hoveringItemNumber;
|
||||
},
|
||||
sqlDialect(): SQLDialectType {
|
||||
return SQL_DIALECTS[this.dialect as keyof typeof SQL_DIALECTS] ?? SQL_DIALECTS.StandardSQL;
|
||||
},
|
||||
extensions(): Extension[] {
|
||||
const dialect = this.sqlDialect;
|
||||
|
||||
function sqlWithN8nLanguageSupport() {
|
||||
return new LanguageSupport(dialect.language, [
|
||||
dialect.language.data.of({
|
||||
autocomplete: ifNotIn(['Resolvable'], keywordCompletionSource(dialect, true)),
|
||||
}),
|
||||
n8nCompletionSources().map((source) => dialect.language.data.of(source)),
|
||||
]);
|
||||
}
|
||||
|
||||
const extensions = [
|
||||
sqlWithN8nLanguageSupport(),
|
||||
expressionInputHandler(),
|
||||
codeNodeEditorTheme({ isReadOnly: this.isReadOnly, customMaxHeight: '350px' }),
|
||||
lineNumbers(),
|
||||
EditorView.lineWrapping,
|
||||
EditorState.readOnly.of(this.isReadOnly),
|
||||
EditorView.domEventHandlers({
|
||||
focus: () => {
|
||||
this.isFocused = true;
|
||||
},
|
||||
}),
|
||||
EditorState.readOnly.of(this.isReadOnly),
|
||||
EditorView.editable.of(!this.isReadOnly),
|
||||
];
|
||||
|
||||
if (!this.isReadOnly) {
|
||||
extensions.push(
|
||||
history(),
|
||||
keymap.of([
|
||||
{ key: 'Mod-Shift-z', run: redo },
|
||||
{ key: 'Mod-/', run: toggleComment },
|
||||
{ key: 'Tab', run: acceptCompletion },
|
||||
indentWithTab,
|
||||
]),
|
||||
autocompletion(),
|
||||
indentOnInput(),
|
||||
highlightActiveLine(),
|
||||
highlightActiveLineGutter(),
|
||||
foldGutter(),
|
||||
dropCursor(),
|
||||
bracketMatching(),
|
||||
EditorView.updateListener.of((viewUpdate) => {
|
||||
if (!viewUpdate.docChanged || !this.editor) return;
|
||||
|
||||
highlighter.removeColor(this.editor as EditorView, this.plaintextSegments);
|
||||
highlighter.addColor(this.editor as EditorView, this.resolvableSegments);
|
||||
|
||||
this.$emit('valueChanged', this.doc);
|
||||
}),
|
||||
);
|
||||
}
|
||||
return extensions;
|
||||
},
|
||||
},
|
||||
|
||||
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) codeNodeEditorEventBus.on('error-line-number', this.highlightLine);
|
||||
|
||||
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 });
|
||||
const state = EditorState.create({ doc: this.query, extensions: this.extensions });
|
||||
this.editor = new EditorView({ parent: this.$refs.sqlEditor as HTMLDivElement, state });
|
||||
highlighter.addColor(this.editor as EditorView, this.resolvableSegments);
|
||||
},
|
||||
methods: {
|
||||
onBlur() {
|
||||
this.isFocused = false;
|
||||
},
|
||||
highlightLine(line: number | 'final') {
|
||||
if (!this.editor) return;
|
||||
|
||||
if (line === 'final') {
|
||||
this.editor.dispatch({
|
||||
selection: { anchor: this.query.length },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.editor.dispatch({
|
||||
selection: { anchor: this.editor.state.doc.line(line).from },
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style module lang="scss">
|
||||
.sqlEditor {
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
|
|
112
packages/editor-ui/src/components/__tests__/SQLEditor.test.ts
Normal file
112
packages/editor-ui/src/components/__tests__/SQLEditor.test.ts
Normal file
|
@ -0,0 +1,112 @@
|
|||
import { render } from '@testing-library/vue';
|
||||
import { PiniaVuePlugin } from 'pinia';
|
||||
import { SETTINGS_STORE_DEFAULT_STATE, waitAllPromises } from '@/__tests__/utils';
|
||||
import { STORES } from '@/constants';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
|
||||
import SqlEditor from '@/components/SqlEditor/SqlEditor.vue';
|
||||
import { expressionManager } from '@/mixins/expressionManager';
|
||||
import type { TargetItem } from '@/Interface';
|
||||
|
||||
const EXPRESSION_OUTPUT_TEST_ID = 'inline-expression-editor-output';
|
||||
|
||||
const RESOLVABLES: { [key: string]: string | number | boolean } = {
|
||||
'{{ $json.schema }}': 'public',
|
||||
'{{ $json.table }}': 'users',
|
||||
'{{ $json.id }}': 'id',
|
||||
'{{ $json.limit - 10 }}': 0,
|
||||
'{{ $json.active }}': false,
|
||||
};
|
||||
|
||||
const DEFAULT_SETUP = {
|
||||
pinia: createTestingPinia({
|
||||
initialState: {
|
||||
[STORES.SETTINGS]: {
|
||||
settings: SETTINGS_STORE_DEFAULT_STATE.settings,
|
||||
},
|
||||
},
|
||||
}),
|
||||
props: {
|
||||
dialect: 'PostgreSQL',
|
||||
isReadOnly: false,
|
||||
},
|
||||
};
|
||||
|
||||
const renderComponent = (renderOptions: Parameters<typeof render>[1] = {}) =>
|
||||
render(SqlEditor, { ...DEFAULT_SETUP, ...renderOptions }, (vue) => {
|
||||
vue.use(PiniaVuePlugin);
|
||||
});
|
||||
|
||||
describe('SQL Editor Preview Tests', () => {
|
||||
beforeEach(() => {
|
||||
vi.spyOn(expressionManager.methods, 'resolve').mockImplementation(
|
||||
(resolvable: string, _targetItem?: TargetItem) => {
|
||||
return { resolved: RESOLVABLES[resolvable] };
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders basic query', async () => {
|
||||
const { getByTestId } = renderComponent({
|
||||
props: {
|
||||
query: 'SELECT * FROM users',
|
||||
},
|
||||
});
|
||||
await waitAllPromises();
|
||||
expect(getByTestId(EXPRESSION_OUTPUT_TEST_ID)).toHaveTextContent('SELECT * FROM users');
|
||||
});
|
||||
|
||||
it('renders basic query with expression', async () => {
|
||||
const { getByTestId } = renderComponent({
|
||||
props: {
|
||||
query: 'SELECT * FROM {{ $json.table }}',
|
||||
},
|
||||
});
|
||||
await waitAllPromises();
|
||||
expect(getByTestId(EXPRESSION_OUTPUT_TEST_ID)).toHaveTextContent('SELECT * FROM users');
|
||||
});
|
||||
|
||||
it('renders resolved expressions with dot between resolvables', async () => {
|
||||
const { getByTestId } = renderComponent({
|
||||
props: {
|
||||
query: 'SELECT * FROM {{ $json.schema }}.{{ $json.table }}',
|
||||
},
|
||||
});
|
||||
await waitAllPromises();
|
||||
expect(getByTestId(EXPRESSION_OUTPUT_TEST_ID)).toHaveTextContent('SELECT * FROM public.users');
|
||||
});
|
||||
|
||||
it('renders resolved expressions which resolve to 0', async () => {
|
||||
const { getByTestId } = renderComponent({
|
||||
props: {
|
||||
query:
|
||||
'SELECT * FROM {{ $json.schema }}.{{ $json.table }} WHERE {{ $json.id }} > {{ $json.limit - 10 }}',
|
||||
},
|
||||
});
|
||||
await waitAllPromises();
|
||||
expect(getByTestId(EXPRESSION_OUTPUT_TEST_ID)).toHaveTextContent(
|
||||
'SELECT * FROM public.users WHERE id > 0',
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps query formatting in rendered output', async () => {
|
||||
const { getByTestId } = renderComponent({
|
||||
props: {
|
||||
query:
|
||||
'SELECT * FROM {{ $json.schema }}.{{ $json.table }}\n WHERE id > {{ $json.limit - 10 }}\n AND active = {{ $json.active }};',
|
||||
},
|
||||
});
|
||||
await waitAllPromises();
|
||||
expect(getByTestId(EXPRESSION_OUTPUT_TEST_ID)).toHaveTextContent(
|
||||
'SELECT * FROM public.users WHERE id > 0 AND active = false;',
|
||||
);
|
||||
// Output should have the same number of lines as the input
|
||||
expect(getByTestId('sql-editor-container').getElementsByClassName('cm-line').length).toEqual(
|
||||
getByTestId(EXPRESSION_OUTPUT_TEST_ID).getElementsByClassName('cm-line').length,
|
||||
);
|
||||
});
|
||||
});
|
|
@ -23,6 +23,7 @@ export const expressionManager = defineComponent({
|
|||
data() {
|
||||
return {
|
||||
editor: {} as EditorView,
|
||||
skipSegments: [] as string[],
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
|
@ -71,6 +72,8 @@ export const expressionManager = defineComponent({
|
|||
},
|
||||
|
||||
segments(): Segment[] {
|
||||
if (!this.editor?.state) return [];
|
||||
|
||||
const rawSegments: RawSegment[] = [];
|
||||
|
||||
const fullTree = ensureSyntaxTree(
|
||||
|
@ -83,14 +86,18 @@ export const expressionManager = defineComponent({
|
|||
throw new Error(`Failed to parse expression: ${this.editor.state.doc.toString()}`);
|
||||
}
|
||||
|
||||
const skipSegments = ['Program', 'Script', 'Document', ...this.skipSegments];
|
||||
|
||||
fullTree.cursor().iterate((node) => {
|
||||
if (node.type.name === 'Program') return;
|
||||
const text = this.editor.state.sliceDoc(node.from, node.to);
|
||||
|
||||
if (skipSegments.includes(node.type.name)) return;
|
||||
|
||||
rawSegments.push({
|
||||
from: node.from,
|
||||
to: node.to,
|
||||
text: this.editor.state.sliceDoc(node.from, node.to),
|
||||
token: node.type.name,
|
||||
text,
|
||||
token: node.type.name === 'Resolvable' ? 'Resolvable' : 'Plaintext',
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -100,7 +107,18 @@ export const expressionManager = defineComponent({
|
|||
if (token === 'Resolvable') {
|
||||
const { resolved, error, fullError } = this.resolve(text, this.hoveringItem);
|
||||
|
||||
acc.push({ kind: 'resolvable', from, to, resolvable: text, resolved, error, fullError });
|
||||
acc.push({
|
||||
kind: 'resolvable',
|
||||
from,
|
||||
to,
|
||||
resolvable: text,
|
||||
// TODO:
|
||||
// For some reason, expressions that resolve to a number 0 are breaking preview in the SQL editor
|
||||
// This fixes that but as as TODO we should figure out why this is happening
|
||||
resolved: String(resolved),
|
||||
error,
|
||||
fullError,
|
||||
});
|
||||
|
||||
return acc;
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import type {
|
|||
IRunDataDisplayMode,
|
||||
NDVState,
|
||||
NodePanelType,
|
||||
TargetItem,
|
||||
XYPosition,
|
||||
} from '@/Interface';
|
||||
import type { INodeIssues, IRunData } from 'n8n-workflow';
|
||||
|
@ -125,6 +126,16 @@ export const useNDVStore = defineStore(STORES.NDV, {
|
|||
const parentNodes = workflow.getParentNodes(this.activeNode.name, 'main', 1);
|
||||
return parentNodes.includes(inputNodeName);
|
||||
},
|
||||
hoveringItemNumber(): number {
|
||||
return (this.hoveringItem?.itemIndex ?? 0) + 1;
|
||||
},
|
||||
getHoveringItem(): TargetItem | null {
|
||||
if (this.isInputParentOfActiveNode) {
|
||||
return this.hoveringItem;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
setInputNodeName(nodeName: string | undefined): void {
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
.cm-tooltip-autocomplete:after {
|
||||
display: block;
|
||||
content: 'n8n supports all JavaScript functions, including those not listed.';
|
||||
padding: var(--spacing-2xs) var(--spacing-s);
|
||||
border-top: 1px solid var(--color-foreground-dark);
|
||||
}
|
||||
|
||||
.code-node-editor .cm-tooltip-autocomplete:after {
|
||||
display: block;
|
||||
content: 'n8n supports all JavaScript functions, including those not listed.';
|
||||
}
|
||||
|
||||
// Custom autocomplete item type icons
|
||||
// 1. Native and n8n extension functions:
|
||||
.cm-completionIcon-extension-function, .cm-completionIcon-native-function {
|
||||
|
|
|
@ -4,5 +4,6 @@ module.exports = {
|
|||
collectCoverageFrom: ['credentials/**/*.ts', 'nodes/**/*.ts', 'utils/**/*.ts'],
|
||||
moduleNameMapper: {
|
||||
'^@test/(.*)$': '<rootDir>/test/$1',
|
||||
'^@utils/(.*)$': '<rootDir>/utils/$1',
|
||||
},
|
||||
};
|
||||
|
|
|
@ -9,7 +9,7 @@ import unset from 'lodash/unset';
|
|||
import { cloneDeep } from 'lodash';
|
||||
import set from 'lodash/set';
|
||||
import union from 'lodash/union';
|
||||
import { fuzzyCompare } from '../../utils/utilities';
|
||||
import { fuzzyCompare } from '@utils/utilities';
|
||||
|
||||
type PairToMatch = {
|
||||
field1: string;
|
||||
|
|
|
@ -11,7 +11,7 @@ import {
|
|||
getItemCopy,
|
||||
getItemsCopy,
|
||||
pgInsert,
|
||||
pgQuery,
|
||||
pgQueryV2,
|
||||
pgUpdate,
|
||||
} from '../Postgres/v1/genericFunctions';
|
||||
|
||||
|
@ -73,9 +73,10 @@ export class CrateDb implements INodeType {
|
|||
displayName: 'Query',
|
||||
name: 'query',
|
||||
type: 'string',
|
||||
noDataExpression: true,
|
||||
typeOptions: {
|
||||
editor: 'sqlEditor',
|
||||
sqlDialect: 'postgres',
|
||||
sqlDialect: 'PostgreSQL',
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
|
@ -283,13 +284,9 @@ export class CrateDb implements INodeType {
|
|||
// executeQuery
|
||||
// ----------------------------------
|
||||
|
||||
const queryResult = await pgQuery(
|
||||
this.getNodeParameter,
|
||||
pgp,
|
||||
db,
|
||||
items,
|
||||
this.continueOnFail(),
|
||||
);
|
||||
const queryResult = await pgQueryV2.call(this, pgp, db, items, this.continueOnFail(), {
|
||||
resolveExpression: true,
|
||||
});
|
||||
|
||||
returnItems = this.helpers.returnJsonArray(queryResult);
|
||||
} else if (operation === 'insert') {
|
||||
|
|
|
@ -10,7 +10,7 @@ import { NodeApiError } from 'n8n-workflow';
|
|||
import { createTransport } from 'nodemailer';
|
||||
import type SMTPTransport from 'nodemailer/lib/smtp-transport';
|
||||
|
||||
import { updateDisplayOptions } from '../../../utils/utilities';
|
||||
import { updateDisplayOptions } from '@utils/utilities';
|
||||
|
||||
const properties: INodeProperties[] = [
|
||||
// TODO: Add choice for text as text or html (maybe also from name)
|
||||
|
|
|
@ -16,7 +16,7 @@ import { googleApiRequest, googleApiRequestAllItems, merge, simplify } from './G
|
|||
import moment from 'moment-timezone';
|
||||
import type { IData } from './Interfaces';
|
||||
|
||||
import { oldVersionNotice } from '../../../../utils/descriptions';
|
||||
import { oldVersionNotice } from '@utils/descriptions';
|
||||
|
||||
const versionDescription: INodeTypeDescription = {
|
||||
displayName: 'Google Analytics',
|
||||
|
|
|
@ -20,7 +20,7 @@ import { recordFields, recordOperations } from './RecordDescription';
|
|||
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { oldVersionNotice } from '../../../../utils/descriptions';
|
||||
import { oldVersionNotice } from '@utils/descriptions';
|
||||
|
||||
const versionDescription: INodeTypeDescription = {
|
||||
displayName: 'Google BigQuery',
|
||||
|
|
|
@ -3,7 +3,7 @@ import type { IExecuteFunctions } from 'n8n-core';
|
|||
import type { IDataObject, INodeExecutionData, INodeProperties } from 'n8n-workflow';
|
||||
|
||||
import { NodeOperationError, sleep } from 'n8n-workflow';
|
||||
import { updateDisplayOptions } from '../../../../../../utils/utilities';
|
||||
import { getResolvables, updateDisplayOptions } from '@utils/utilities';
|
||||
import type { JobInsertResponse } from '../../helpers/interfaces';
|
||||
|
||||
import { prepareOutput } from '../../helpers/utils';
|
||||
|
@ -14,6 +14,7 @@ const properties: INodeProperties[] = [
|
|||
displayName: 'SQL Query',
|
||||
name: 'sqlQuery',
|
||||
type: 'string',
|
||||
noDataExpression: true,
|
||||
typeOptions: {
|
||||
editor: 'sqlEditor',
|
||||
},
|
||||
|
@ -31,6 +32,7 @@ const properties: INodeProperties[] = [
|
|||
displayName: 'SQL Query',
|
||||
name: 'sqlQuery',
|
||||
type: 'string',
|
||||
noDataExpression: true,
|
||||
typeOptions: {
|
||||
editor: 'sqlEditor',
|
||||
},
|
||||
|
@ -160,12 +162,16 @@ export async function execute(this: IExecuteFunctions): Promise<INodeExecutionDa
|
|||
|
||||
for (let i = 0; i < length; i++) {
|
||||
try {
|
||||
const sqlQuery = this.getNodeParameter('sqlQuery', i) as string;
|
||||
let sqlQuery = this.getNodeParameter('sqlQuery', i) as string;
|
||||
const options = this.getNodeParameter('options', i);
|
||||
const projectId = this.getNodeParameter('projectId', i, undefined, {
|
||||
extractValue: true,
|
||||
});
|
||||
|
||||
for (const resolvable of getResolvables(sqlQuery)) {
|
||||
sqlQuery = sqlQuery.replace(resolvable, this.evaluateExpression(resolvable, i) as string);
|
||||
}
|
||||
|
||||
let rawOutput = false;
|
||||
let includeSchema = false;
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import type { IExecuteFunctions } from 'n8n-core';
|
|||
import type { IDataObject, INodeExecutionData, INodeProperties } from 'n8n-workflow';
|
||||
import { NodeOperationError } from 'n8n-workflow';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { updateDisplayOptions } from '../../../../../../utils/utilities';
|
||||
import { updateDisplayOptions } from '@utils/utilities';
|
||||
import type { TableSchema } from '../../helpers/interfaces';
|
||||
import { checkSchema, wrapData } from '../../helpers/utils';
|
||||
import { googleApiRequest } from '../../transport';
|
||||
|
|
|
@ -30,7 +30,7 @@ import { draftFields, draftOperations } from './DraftDescription';
|
|||
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
|
||||
import { oldVersionNotice } from '../../../../utils/descriptions';
|
||||
import { oldVersionNotice } from '@utils/descriptions';
|
||||
|
||||
const versionDescription: INodeTypeDescription = {
|
||||
displayName: 'Gmail',
|
||||
|
|
|
@ -8,7 +8,8 @@ import type {
|
|||
} from 'n8n-workflow';
|
||||
import { NodeOperationError } from 'n8n-workflow';
|
||||
import { placeholder } from './placeholder';
|
||||
import { getResolvables, getValue } from './utils';
|
||||
import { getValue } from './utils';
|
||||
import { getResolvables } from '@utils/utilities';
|
||||
import type { IValueData } from './types';
|
||||
|
||||
export class Html implements INodeType {
|
||||
|
|
|
@ -1,26 +1,6 @@
|
|||
import type { IDataObject } from 'n8n-workflow';
|
||||
import type { IValueData, Cheerio } from './types';
|
||||
|
||||
/**
|
||||
* @TECH_DEBT Explore replacing with handlebars
|
||||
*/
|
||||
export function getResolvables(html: string) {
|
||||
if (!html) return [];
|
||||
|
||||
const resolvables = [];
|
||||
const resolvableRegex = /({{[\s\S]*?}})/g;
|
||||
|
||||
let match;
|
||||
|
||||
while ((match = resolvableRegex.exec(html)) !== null) {
|
||||
if (match[1]) {
|
||||
resolvables.push(match[1]);
|
||||
}
|
||||
}
|
||||
|
||||
return resolvables;
|
||||
}
|
||||
|
||||
// The extraction functions
|
||||
const extractFunctions: {
|
||||
[key: string]: ($: Cheerio, valueData: IValueData) => string | undefined;
|
||||
|
|
|
@ -24,7 +24,7 @@ import {
|
|||
replaceNullValues,
|
||||
sanitizeUiMessage,
|
||||
} from '../GenericFunctions';
|
||||
import { keysToLowercase } from '../../../utils/utilities';
|
||||
import { keysToLowercase } from '@utils/utilities';
|
||||
|
||||
function toText<T>(data: T) {
|
||||
if (typeof data === 'object' && data !== null) {
|
||||
|
|
|
@ -12,7 +12,7 @@ import type {
|
|||
} from 'n8n-workflow';
|
||||
import { deepCopy } from 'n8n-workflow';
|
||||
|
||||
import { oldVersionNotice } from '../../../utils/descriptions';
|
||||
import { oldVersionNotice } from '@utils/descriptions';
|
||||
|
||||
const versionDescription: INodeTypeDescription = {
|
||||
displayName: 'Merge',
|
||||
|
|
|
@ -11,7 +11,7 @@ import assignWith from 'lodash/assignWith';
|
|||
import get from 'lodash/get';
|
||||
import merge from 'lodash/merge';
|
||||
import mergeWith from 'lodash/mergeWith';
|
||||
import { fuzzyCompare } from '../../../utils/utilities';
|
||||
import { fuzzyCompare } from '@utils/utilities';
|
||||
|
||||
type PairToMatch = {
|
||||
field1: string;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import type { IExecuteFunctions } from 'n8n-core';
|
||||
import type { IDataObject, INodeExecutionData, INodeProperties } from 'n8n-workflow';
|
||||
import { updateDisplayOptions } from '../../../../../../utils/utilities';
|
||||
import { updateDisplayOptions } from '@utils/utilities';
|
||||
import { microsoftApiRequest } from '../../transport';
|
||||
import { workbookRLC, worksheetRLC } from '../common.descriptions';
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import type { IExecuteFunctions } from 'n8n-core';
|
||||
import type { IDataObject, INodeExecutionData, INodeProperties } from 'n8n-workflow';
|
||||
import { processJsonInput, updateDisplayOptions } from '../../../../../../utils/utilities';
|
||||
import { processJsonInput, updateDisplayOptions } from '@utils/utilities';
|
||||
import type { ExcelResponse } from '../../helpers/interfaces';
|
||||
import { prepareOutput } from '../../helpers/utils';
|
||||
import { microsoftApiRequest } from '../../transport';
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import type { IExecuteFunctions } from 'n8n-core';
|
||||
import type { IDataObject, INodeExecutionData, INodeProperties } from 'n8n-workflow';
|
||||
import { updateDisplayOptions } from '../../../../../../utils/utilities';
|
||||
import { updateDisplayOptions } from '@utils/utilities';
|
||||
import { microsoftApiRequest } from '../../transport';
|
||||
import { tableRLC, workbookRLC, worksheetRLC } from '../common.descriptions';
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import type { IExecuteFunctions } from 'n8n-core';
|
||||
import type { INodeExecutionData, INodeProperties } from 'n8n-workflow';
|
||||
import { updateDisplayOptions } from '../../../../../../utils/utilities';
|
||||
import { updateDisplayOptions } from '@utils/utilities';
|
||||
import { microsoftApiRequest } from '../../transport';
|
||||
import { tableRLC, workbookRLC, worksheetRLC } from '../common.descriptions';
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import type { IExecuteFunctions } from 'n8n-core';
|
||||
import type { IDataObject, INodeExecutionData, INodeProperties } from 'n8n-workflow';
|
||||
import { updateDisplayOptions } from '../../../../../../utils/utilities';
|
||||
import { updateDisplayOptions } from '@utils/utilities';
|
||||
import { microsoftApiRequest, microsoftApiRequestAllItemsSkip } from '../../transport';
|
||||
import { tableRLC, workbookRLC, worksheetRLC } from '../common.descriptions';
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import type { IExecuteFunctions } from 'n8n-core';
|
||||
import type { IDataObject, INodeExecutionData, INodeProperties } from 'n8n-workflow';
|
||||
import { updateDisplayOptions } from '../../../../../../utils/utilities';
|
||||
import { updateDisplayOptions } from '@utils/utilities';
|
||||
import { microsoftApiRequest, microsoftApiRequestAllItemsSkip } from '../../transport';
|
||||
import { tableRLC, workbookRLC, worksheetRLC } from '../common.descriptions';
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import type { IExecuteFunctions } from 'n8n-core';
|
||||
import type { IDataObject, INodeExecutionData, INodeProperties, JsonObject } from 'n8n-workflow';
|
||||
import { NodeApiError } from 'n8n-workflow';
|
||||
import { updateDisplayOptions } from '../../../../../../utils/utilities';
|
||||
import { updateDisplayOptions } from '@utils/utilities';
|
||||
import { microsoftApiRequestAllItemsSkip } from '../../transport';
|
||||
import { tableRLC, workbookRLC, worksheetRLC } from '../common.descriptions';
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import type { IExecuteFunctions } from 'n8n-core';
|
||||
import type { IDataObject, INodeExecutionData, INodeProperties } from 'n8n-workflow';
|
||||
import { updateDisplayOptions } from '../../../../../../utils/utilities';
|
||||
import { updateDisplayOptions } from '@utils/utilities';
|
||||
import { microsoftApiRequest } from '../../transport';
|
||||
import { workbookRLC } from '../common.descriptions';
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import type { IExecuteFunctions } from 'n8n-core';
|
||||
import type { INodeExecutionData, INodeProperties } from 'n8n-workflow';
|
||||
import { NodeOperationError } from 'n8n-workflow';
|
||||
import { updateDisplayOptions } from '../../../../../../utils/utilities';
|
||||
import { updateDisplayOptions } from '@utils/utilities';
|
||||
import { microsoftApiRequest } from '../../transport';
|
||||
import { workbookRLC } from '../common.descriptions';
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import type { IExecuteFunctions } from 'n8n-core';
|
||||
import type { IDataObject, INodeExecutionData, INodeProperties } from 'n8n-workflow';
|
||||
import { updateDisplayOptions } from '../../../../../../utils/utilities';
|
||||
import { updateDisplayOptions } from '@utils/utilities';
|
||||
import { microsoftApiRequest, microsoftApiRequestAllItems } from '../../transport';
|
||||
|
||||
const properties: INodeProperties[] = [
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import type { IExecuteFunctions } from 'n8n-core';
|
||||
import type { IDataObject, INodeExecutionData, INodeProperties } from 'n8n-workflow';
|
||||
import { processJsonInput, updateDisplayOptions } from '../../../../../../utils/utilities';
|
||||
import { processJsonInput, updateDisplayOptions } from '@utils/utilities';
|
||||
import type { ExcelResponse } from '../../helpers/interfaces';
|
||||
import { prepareOutput } from '../../helpers/utils';
|
||||
import { microsoftApiRequest } from '../../transport';
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import type { IExecuteFunctions } from 'n8n-core';
|
||||
import type { INodeExecutionData, INodeProperties } from 'n8n-workflow';
|
||||
import { updateDisplayOptions } from '../../../../../../utils/utilities';
|
||||
import { updateDisplayOptions } from '@utils/utilities';
|
||||
import { microsoftApiRequest } from '../../transport';
|
||||
import { workbookRLC, worksheetRLC } from '../common.descriptions';
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import type { IExecuteFunctions } from 'n8n-core';
|
||||
import type { INodeExecutionData, INodeProperties } from 'n8n-workflow';
|
||||
import { updateDisplayOptions } from '../../../../../../utils/utilities';
|
||||
import { updateDisplayOptions } from '@utils/utilities';
|
||||
import { microsoftApiRequest } from '../../transport';
|
||||
import { workbookRLC, worksheetRLC } from '../common.descriptions';
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import type { IExecuteFunctions } from 'n8n-core';
|
||||
import type { IDataObject, INodeExecutionData, INodeProperties } from 'n8n-workflow';
|
||||
import { updateDisplayOptions } from '../../../../../../utils/utilities';
|
||||
import { updateDisplayOptions } from '@utils/utilities';
|
||||
import { microsoftApiRequest, microsoftApiRequestAllItems } from '../../transport';
|
||||
import { workbookRLC } from '../common.descriptions';
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import type { IExecuteFunctions } from 'n8n-core';
|
||||
import type { IDataObject, INodeExecutionData, INodeProperties } from 'n8n-workflow';
|
||||
import { updateDisplayOptions } from '../../../../../../utils/utilities';
|
||||
import { updateDisplayOptions } from '@utils/utilities';
|
||||
import type { ExcelResponse } from '../../helpers/interfaces';
|
||||
import { prepareOutput } from '../../helpers/utils';
|
||||
import { microsoftApiRequest } from '../../transport';
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import type { IExecuteFunctions } from 'n8n-core';
|
||||
import type { IDataObject, INodeExecutionData, INodeProperties } from 'n8n-workflow';
|
||||
import { NodeOperationError } from 'n8n-workflow';
|
||||
import { processJsonInput, updateDisplayOptions } from '../../../../../../utils/utilities';
|
||||
import { processJsonInput, updateDisplayOptions } from '@utils/utilities';
|
||||
import type { ExcelResponse, UpdateSummary } from '../../helpers/interfaces';
|
||||
import { prepareOutput, updateByAutoMaping, updateByDefinedValues } from '../../helpers/utils';
|
||||
import { microsoftApiRequest } from '../../transport';
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import type { IExecuteFunctions } from 'n8n-core';
|
||||
import type { IDataObject, INodeExecutionData, INodeProperties } from 'n8n-workflow';
|
||||
import { NodeOperationError } from 'n8n-workflow';
|
||||
import { processJsonInput, updateDisplayOptions } from '../../../../../../utils/utilities';
|
||||
import { processJsonInput, updateDisplayOptions } from '@utils/utilities';
|
||||
import type { ExcelResponse, UpdateSummary } from '../../helpers/interfaces';
|
||||
import { prepareOutput, updateByAutoMaping, updateByDefinedValues } from '../../helpers/utils';
|
||||
import { microsoftApiRequest } from '../../transport';
|
||||
|
|
|
@ -3,7 +3,7 @@ import type { IDataObject, INode, INodeExecutionData } from 'n8n-workflow';
|
|||
import { NodeOperationError } from 'n8n-workflow';
|
||||
import type { ExcelResponse, SheetData, UpdateSummary } from './interfaces';
|
||||
import { constructExecutionMetaData } from 'n8n-core';
|
||||
import { wrapData } from '../../../../../utils/utilities';
|
||||
import { wrapData } from '@utils/utilities';
|
||||
|
||||
type PrepareOutputConfig = {
|
||||
rawData: boolean;
|
||||
|
|
|
@ -11,7 +11,7 @@ import type {
|
|||
} from 'n8n-workflow';
|
||||
import { NodeOperationError } from 'n8n-workflow';
|
||||
|
||||
import { chunk, flatten } from '../../../utils/utilities';
|
||||
import { chunk, flatten, getResolvables } from '@utils/utilities';
|
||||
|
||||
import mssql from 'mssql';
|
||||
|
||||
|
@ -90,9 +90,10 @@ export class MicrosoftSql implements INodeType {
|
|||
displayName: 'Query',
|
||||
name: 'query',
|
||||
type: 'string',
|
||||
noDataExpression: true,
|
||||
typeOptions: {
|
||||
editor: 'sqlEditor',
|
||||
sqlDialect: 'mssql',
|
||||
sqlDialect: 'MSSQL',
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
|
@ -293,7 +294,11 @@ export class MicrosoftSql implements INodeType {
|
|||
// executeQuery
|
||||
// ----------------------------------
|
||||
|
||||
const rawQuery = this.getNodeParameter('query', 0) as string;
|
||||
let rawQuery = this.getNodeParameter('query', 0) as string;
|
||||
|
||||
for (const resolvable of getResolvables(rawQuery)) {
|
||||
rawQuery = rawQuery.replace(resolvable, this.evaluateExpression(resolvable, 0) as string);
|
||||
}
|
||||
|
||||
const queryResult = await pool.request().query(rawQuery);
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ import type mysql2 from 'mysql2/promise';
|
|||
import { copyInputItems, createConnection, searchTables } from './GenericFunctions';
|
||||
import type { IExecuteFunctions } from 'n8n-core';
|
||||
|
||||
import { oldVersionNotice } from '../../../utils/descriptions';
|
||||
import { oldVersionNotice } from '@utils/descriptions';
|
||||
|
||||
const versionDescription: INodeTypeDescription = {
|
||||
displayName: 'MySQL',
|
||||
|
@ -75,9 +75,10 @@ const versionDescription: INodeTypeDescription = {
|
|||
displayName: 'Query',
|
||||
name: 'query',
|
||||
type: 'string',
|
||||
noDataExpression: true,
|
||||
typeOptions: {
|
||||
editor: 'sqlEditor',
|
||||
sqlDialect: 'mysql',
|
||||
sqlDialect: 'MySQL',
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
|
|
|
@ -9,7 +9,7 @@ import type {
|
|||
WhereClause,
|
||||
} from '../../helpers/interfaces';
|
||||
|
||||
import { updateDisplayOptions } from '../../../../../utils/utilities';
|
||||
import { updateDisplayOptions } from '@utils/utilities';
|
||||
|
||||
import { addWhereClauses } from '../../helpers/utils';
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import { NodeOperationError } from 'n8n-workflow';
|
|||
|
||||
import type { QueryRunner, QueryWithValues } from '../../helpers/interfaces';
|
||||
|
||||
import { updateDisplayOptions } from '../../../../../utils/utilities';
|
||||
import { getResolvables, updateDisplayOptions } from '@utils/utilities';
|
||||
|
||||
import { prepareQueryAndReplacements, replaceEmptyStringsByNulls } from '../../helpers/utils';
|
||||
|
||||
|
@ -20,9 +20,10 @@ const properties: INodeProperties[] = [
|
|||
required: true,
|
||||
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.",
|
||||
noDataExpression: true,
|
||||
typeOptions: {
|
||||
editor: 'sqlEditor',
|
||||
sqlDialect: 'mysql',
|
||||
sqlDialect: 'MySQL',
|
||||
},
|
||||
hint: 'Prefer using query parameters over n8n expressions to avoid SQL injection attacks',
|
||||
},
|
||||
|
@ -58,7 +59,11 @@ export async function execute(
|
|||
const queries: QueryWithValues[] = [];
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const rawQuery = this.getNodeParameter('query', i) as string;
|
||||
let rawQuery = this.getNodeParameter('query', i) as string;
|
||||
|
||||
for (const resolvable of getResolvables(rawQuery)) {
|
||||
rawQuery = rawQuery.replace(resolvable, this.evaluateExpression(resolvable, i) as string);
|
||||
}
|
||||
|
||||
const options = this.getNodeParameter('options', i, {});
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ import type {
|
|||
|
||||
import { AUTO_MAP, BATCH_MODE, DATA_MODE } from '../../helpers/interfaces';
|
||||
|
||||
import { updateDisplayOptions } from '../../../../../utils/utilities';
|
||||
import { updateDisplayOptions } from '@utils/utilities';
|
||||
|
||||
import { copyInputItems, replaceEmptyStringsByNulls } from '../../helpers/utils';
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ import type {
|
|||
WhereClause,
|
||||
} from '../../helpers/interfaces';
|
||||
|
||||
import { updateDisplayOptions } from '../../../../../utils/utilities';
|
||||
import { updateDisplayOptions } from '@utils/utilities';
|
||||
|
||||
import { addSortRules, addWhereClauses } from '../../helpers/utils';
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import type { IDataObject, INodeExecutionData, INodeProperties } from 'n8n-workf
|
|||
import type { QueryRunner, QueryValues, QueryWithValues } from '../../helpers/interfaces';
|
||||
import { AUTO_MAP, DATA_MODE } from '../../helpers/interfaces';
|
||||
|
||||
import { updateDisplayOptions } from '../../../../../utils/utilities';
|
||||
import { updateDisplayOptions } from '@utils/utilities';
|
||||
|
||||
import { replaceEmptyStringsByNulls } from '../../helpers/utils';
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import type { IDataObject, INodeExecutionData, INodeProperties } from 'n8n-workf
|
|||
import type { QueryRunner, QueryValues, QueryWithValues } from '../../helpers/interfaces';
|
||||
import { AUTO_MAP, DATA_MODE } from '../../helpers/interfaces';
|
||||
|
||||
import { updateDisplayOptions } from '../../../../../utils/utilities';
|
||||
import { updateDisplayOptions } from '@utils/utilities';
|
||||
|
||||
import { replaceEmptyStringsByNulls } from '../../helpers/utils';
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ import pgPromise from 'pg-promise';
|
|||
|
||||
import { pgInsertV2, pgQueryV2, pgUpdate, wrapData } from './genericFunctions';
|
||||
|
||||
import { oldVersionNotice } from '../../../utils/descriptions';
|
||||
import { oldVersionNotice } from '@utils/descriptions';
|
||||
|
||||
const versionDescription: INodeTypeDescription = {
|
||||
displayName: 'Postgres',
|
||||
|
@ -74,9 +74,10 @@ const versionDescription: INodeTypeDescription = {
|
|||
displayName: 'Query',
|
||||
name: 'query',
|
||||
type: 'string',
|
||||
noDataExpression: true,
|
||||
typeOptions: {
|
||||
editor: 'sqlEditor',
|
||||
sqlDialect: 'postgres',
|
||||
sqlDialect: 'PostgreSQL',
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import type { IExecuteFunctions, IDataObject, INodeExecutionData, JsonObject } from 'n8n-workflow';
|
||||
import type pgPromise from 'pg-promise';
|
||||
import type pg from 'pg-promise/typescript/pg-subset';
|
||||
import { getResolvables } from '@utils/utilities';
|
||||
|
||||
/**
|
||||
* Returns of a shallow copy of the items which only contains the json data and
|
||||
|
@ -168,7 +169,10 @@ export async function pgQueryV2(
|
|||
db: pgPromise.IDatabase<{}, pg.IClient>,
|
||||
items: INodeExecutionData[],
|
||||
continueOnFail: boolean,
|
||||
overrideMode?: string,
|
||||
options?: {
|
||||
overrideMode?: string;
|
||||
resolveExpression?: boolean;
|
||||
},
|
||||
): Promise<IDataObject[]> {
|
||||
const additionalFields = this.getNodeParameter('additionalFields', 0);
|
||||
|
||||
|
@ -183,13 +187,22 @@ export async function pgQueryV2(
|
|||
type QueryWithValues = { query: string; values?: string[] };
|
||||
const allQueries = new Array<QueryWithValues>();
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const query = this.getNodeParameter('query', i) as string;
|
||||
let query = this.getNodeParameter('query', i) as string;
|
||||
|
||||
if (options?.resolveExpression) {
|
||||
for (const resolvable of getResolvables(query)) {
|
||||
query = query.replace(resolvable, this.evaluateExpression(resolvable, i) as string);
|
||||
}
|
||||
}
|
||||
|
||||
const values = valuesArray[i];
|
||||
const queryFormat = { query, values };
|
||||
allQueries.push(queryFormat);
|
||||
}
|
||||
|
||||
const mode = overrideMode ? overrideMode : ((additionalFields.mode ?? 'multiple') as string);
|
||||
const mode = options?.overrideMode
|
||||
? options.overrideMode
|
||||
: ((additionalFields.mode ?? 'multiple') as string);
|
||||
if (mode === 'multiple') {
|
||||
return (await db.multi(pgp.helpers.concat(allQueries)))
|
||||
.map((result, i) => {
|
||||
|
|
|
@ -2,7 +2,7 @@ import type { IExecuteFunctions } from 'n8n-core';
|
|||
import type { IDataObject, INodeExecutionData, INodeProperties } from 'n8n-workflow';
|
||||
import { NodeOperationError } from 'n8n-workflow';
|
||||
|
||||
import { updateDisplayOptions } from '../../../../../utils/utilities';
|
||||
import { updateDisplayOptions } from '@utils/utilities';
|
||||
|
||||
import type {
|
||||
PgpDatabase,
|
||||
|
|
|
@ -2,7 +2,7 @@ import type { IExecuteFunctions } from 'n8n-core';
|
|||
import type { IDataObject, INodeExecutionData, INodeProperties } from 'n8n-workflow';
|
||||
import { NodeOperationError } from 'n8n-workflow';
|
||||
|
||||
import { updateDisplayOptions } from '../../../../../utils/utilities';
|
||||
import { getResolvables, updateDisplayOptions } from '@utils/utilities';
|
||||
|
||||
import type { PgpDatabase, QueriesRunner, QueryWithValues } from '../../helpers/interfaces';
|
||||
|
||||
|
@ -17,18 +17,19 @@ const properties: INodeProperties[] = [
|
|||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'e.g. SELECT id, name FROM product WHERE quantity > $1 AND price <= $2',
|
||||
noDataExpression: true,
|
||||
required: true,
|
||||
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: {
|
||||
editor: 'sqlEditor',
|
||||
sqlDialect: 'postgres',
|
||||
sqlDialect: 'PostgreSQL',
|
||||
},
|
||||
hint: 'Prefer using query parameters over n8n expressions to avoid SQL injection attacks',
|
||||
},
|
||||
{
|
||||
displayName: `
|
||||
To use query parameters in your SQL query, reference them as $1, $2, $3, etc in the corresponding order. <a href="https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.postgres/#use-query-parameters">More info</a>.
|
||||
To use query parameters in your SQL query, reference them as $1, $2, $3, etc in the corresponding order. <a href="https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.postgres/#use-query-parameters" target="_blank">More info</a>.
|
||||
`,
|
||||
name: 'notice',
|
||||
type: 'notice',
|
||||
|
@ -58,7 +59,11 @@ export async function execute(
|
|||
const queries: QueryWithValues[] = [];
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const query = this.getNodeParameter('query', i) as string;
|
||||
let query = this.getNodeParameter('query', i) as string;
|
||||
|
||||
for (const resolvable of getResolvables(query)) {
|
||||
query = query.replace(resolvable, this.evaluateExpression(resolvable, i) as string);
|
||||
}
|
||||
|
||||
let values: IDataObject[] = [];
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import type { IExecuteFunctions } from 'n8n-core';
|
||||
import type { IDataObject, INodeExecutionData, INodeProperties } from 'n8n-workflow';
|
||||
|
||||
import { updateDisplayOptions } from '../../../../../utils/utilities';
|
||||
import { updateDisplayOptions } from '@utils/utilities';
|
||||
|
||||
import type {
|
||||
PgpDatabase,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import type { IExecuteFunctions } from 'n8n-core';
|
||||
import type { IDataObject, INodeExecutionData, INodeProperties } from 'n8n-workflow';
|
||||
|
||||
import { updateDisplayOptions } from '../../../../../utils/utilities';
|
||||
import { updateDisplayOptions } from '@utils/utilities';
|
||||
|
||||
import type {
|
||||
PgpDatabase,
|
||||
|
|
|
@ -2,7 +2,7 @@ import type { IExecuteFunctions } from 'n8n-core';
|
|||
import type { IDataObject, INodeExecutionData, INodeProperties } from 'n8n-workflow';
|
||||
import { NodeOperationError } from 'n8n-workflow';
|
||||
|
||||
import { updateDisplayOptions } from '../../../../../utils/utilities';
|
||||
import { updateDisplayOptions } from '@utils/utilities';
|
||||
|
||||
import type {
|
||||
PgpDatabase,
|
||||
|
|
|
@ -2,7 +2,7 @@ import type { IExecuteFunctions } from 'n8n-core';
|
|||
import type { IDataObject, INodeExecutionData, INodeProperties } from 'n8n-workflow';
|
||||
import { NodeOperationError } from 'n8n-workflow';
|
||||
|
||||
import { updateDisplayOptions } from '../../../../../utils/utilities';
|
||||
import { updateDisplayOptions } from '@utils/utilities';
|
||||
|
||||
import type {
|
||||
PgpDatabase,
|
||||
|
|
|
@ -8,7 +8,7 @@ import { NodeOperationError } from 'n8n-workflow';
|
|||
|
||||
import pgPromise from 'pg-promise';
|
||||
|
||||
import { pgInsert, pgQuery } from '../Postgres/v1/genericFunctions';
|
||||
import { pgInsert, pgQueryV2 } from '../Postgres/v1/genericFunctions';
|
||||
|
||||
export class QuestDb implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
|
@ -60,9 +60,10 @@ export class QuestDb implements INodeType {
|
|||
displayName: 'Query',
|
||||
name: 'query',
|
||||
type: 'string',
|
||||
noDataExpression: true,
|
||||
typeOptions: {
|
||||
editor: 'sqlEditor',
|
||||
sqlDialect: 'postgres',
|
||||
sqlDialect: 'PostgreSQL',
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
|
@ -225,14 +226,10 @@ export class QuestDb implements INodeType {
|
|||
const additionalFields = this.getNodeParameter('additionalFields', 0);
|
||||
const mode = (additionalFields.mode || 'independently') as string;
|
||||
|
||||
const queryResult = await pgQuery(
|
||||
this.getNodeParameter,
|
||||
pgp,
|
||||
db,
|
||||
items,
|
||||
this.continueOnFail(),
|
||||
mode,
|
||||
);
|
||||
const queryResult = await pgQueryV2.call(this, pgp, db, items, this.continueOnFail(), {
|
||||
overrideMode: mode,
|
||||
resolveExpression: true,
|
||||
});
|
||||
|
||||
returnItems = this.helpers.returnJsonArray(queryResult);
|
||||
} else if (operation === 'insert') {
|
||||
|
|
|
@ -22,7 +22,7 @@ import { userProfileFields, userProfileOperations } from './UserProfileDescripti
|
|||
import { slackApiRequest, slackApiRequestAllItems, validateJSON } from './GenericFunctions';
|
||||
import type { IAttachment } from './MessageInterface';
|
||||
|
||||
import { oldVersionNotice } from '../../../utils/descriptions';
|
||||
import { oldVersionNotice } from '@utils/descriptions';
|
||||
|
||||
import moment from 'moment';
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ import type {
|
|||
import { connect, copyInputItems, destroy, execute } from './GenericFunctions';
|
||||
|
||||
import snowflake from 'snowflake-sdk';
|
||||
import { getResolvables } from '@utils/utilities';
|
||||
|
||||
export class Snowflake implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
|
@ -65,6 +66,7 @@ export class Snowflake implements INodeType {
|
|||
displayName: 'Query',
|
||||
name: 'query',
|
||||
type: 'string',
|
||||
noDataExpression: true,
|
||||
typeOptions: {
|
||||
editor: 'sqlEditor',
|
||||
},
|
||||
|
@ -178,7 +180,12 @@ export class Snowflake implements INodeType {
|
|||
// ----------------------------------
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const query = this.getNodeParameter('query', i) as string;
|
||||
let query = this.getNodeParameter('query', i) as string;
|
||||
|
||||
for (const resolvable of getResolvables(query)) {
|
||||
query = query.replace(resolvable, this.evaluateExpression(resolvable, i) as string);
|
||||
}
|
||||
|
||||
responseData = await execute(connection, query, []);
|
||||
const executionData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray(responseData as IDataObject[]),
|
||||
|
|
|
@ -6,7 +6,7 @@ import type {
|
|||
} from 'n8n-workflow';
|
||||
import { NodeOperationError } from 'n8n-workflow';
|
||||
|
||||
import { pgInsert, pgQuery, pgUpdate } from '../Postgres/v1/genericFunctions';
|
||||
import { pgInsert, pgQueryV2, pgUpdate } from '../Postgres/v1/genericFunctions';
|
||||
|
||||
import pgPromise from 'pg-promise';
|
||||
|
||||
|
@ -65,9 +65,10 @@ export class TimescaleDb implements INodeType {
|
|||
displayName: 'Query',
|
||||
name: 'query',
|
||||
type: 'string',
|
||||
noDataExpression: true,
|
||||
typeOptions: {
|
||||
editor: 'sqlEditor',
|
||||
sqlDialect: 'postgres',
|
||||
sqlDialect: 'PostgreSQL',
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
|
@ -279,13 +280,9 @@ export class TimescaleDb implements INodeType {
|
|||
// executeQuery
|
||||
// ----------------------------------
|
||||
|
||||
const queryResult = await pgQuery(
|
||||
this.getNodeParameter,
|
||||
pgp,
|
||||
db,
|
||||
items,
|
||||
this.continueOnFail(),
|
||||
);
|
||||
const queryResult = await pgQueryV2.call(this, pgp, db, items, this.continueOnFail(), {
|
||||
resolveExpression: true,
|
||||
});
|
||||
|
||||
returnItems = this.helpers.returnJsonArray(queryResult);
|
||||
} else if (operation === 'insert') {
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
"clean": "rimraf dist .turbo",
|
||||
"dev": "pnpm watch",
|
||||
"typecheck": "tsc",
|
||||
"build": "tsc -p tsconfig.build.json && gulp build:icons && gulp build:translations && pnpm build:metadata",
|
||||
"build": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json && gulp build:icons && gulp build:translations && pnpm build:metadata",
|
||||
"build:translations": "gulp build:translations",
|
||||
"build:metadata": "pnpm n8n-generate-known && pnpm n8n-generate-ui-types",
|
||||
"format": "prettier --write . --ignore-path ../../.prettierignore",
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { fuzzyCompare, keysToLowercase, wrapData } from '../../utils/utilities';
|
||||
import { fuzzyCompare, getResolvables, keysToLowercase, wrapData } from '@utils/utilities';
|
||||
|
||||
//most test cases for fuzzyCompare are done in Compare Datasets node tests
|
||||
describe('Test fuzzyCompare', () => {
|
||||
|
@ -101,3 +101,39 @@ describe('Test keysToLowercase', () => {
|
|||
expect(test6).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test getResolvables', () => {
|
||||
it('should return empty array when there are no resolvables', () => {
|
||||
expect(getResolvables('Plain String, no resolvables here.')).toEqual([]);
|
||||
});
|
||||
it('should properly handle resovables in SQL query', () => {
|
||||
expect(getResolvables('SELECT * FROM {{ $json.db }}.{{ $json.table }};')).toEqual([
|
||||
'{{ $json.db }}',
|
||||
'{{ $json.table }}',
|
||||
]);
|
||||
});
|
||||
it('should properly handle resovables in HTML string', () => {
|
||||
expect(
|
||||
getResolvables(
|
||||
`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><title>{{ $json.pageTitle }}</title></head>
|
||||
<body><h1>{{ $json.heading }}</h1></body>
|
||||
<html>
|
||||
<style>
|
||||
body { height: {{ $json.pageHeight }}; }
|
||||
</style>
|
||||
<script>
|
||||
console.log('{{ $json.welcomeMessage }}');
|
||||
</script>
|
||||
`,
|
||||
),
|
||||
).toEqual([
|
||||
'{{ $json.pageTitle }}',
|
||||
'{{ $json.heading }}',
|
||||
'{{ $json.pageHeight }}',
|
||||
'{{ $json.welcomeMessage }}',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,7 +5,8 @@
|
|||
"types": ["node", "jest"],
|
||||
"noEmit": true,
|
||||
"paths": {
|
||||
"@test/*": ["./test/*"]
|
||||
"@test/*": ["./test/*"],
|
||||
"@utils/*": ["./utils/*"]
|
||||
},
|
||||
// TODO: remove all options below this line
|
||||
"noImplicitReturns": false,
|
||||
|
@ -16,5 +17,12 @@
|
|||
"references": [
|
||||
{ "path": "../workflow/tsconfig.build.json" },
|
||||
{ "path": "../core/tsconfig.build.json" }
|
||||
]
|
||||
],
|
||||
"tsc-alias": {
|
||||
"replacers": {
|
||||
"base-url": {
|
||||
"enabled": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -214,3 +214,23 @@ export const keysToLowercase = <T>(headers: T) => {
|
|||
return acc;
|
||||
}, {} as IDataObject);
|
||||
};
|
||||
|
||||
/**
|
||||
* @TECH_DEBT Explore replacing with handlebars
|
||||
*/
|
||||
export function getResolvables(expression: string) {
|
||||
if (!expression) return [];
|
||||
|
||||
const resolvables = [];
|
||||
const resolvableRegex = /({{[\s\S]*?}})/g;
|
||||
|
||||
let match;
|
||||
|
||||
while ((match = resolvableRegex.exec(expression)) !== null) {
|
||||
if (match[1]) {
|
||||
resolvables.push(match[1]);
|
||||
}
|
||||
}
|
||||
|
||||
return resolvables;
|
||||
}
|
||||
|
|
|
@ -1025,7 +1025,15 @@ export type CodeAutocompleteTypes = 'function' | 'functionItem';
|
|||
export type EditorType = 'code' | 'codeNodeEditor' | 'htmlEditor' | 'sqlEditor' | 'json';
|
||||
export type CodeNodeEditorLanguage = (typeof CODE_LANGUAGES)[number];
|
||||
export type CodeExecutionMode = (typeof CODE_EXECUTION_MODES)[number];
|
||||
export type SQLDialect = 'mssql' | 'mysql' | 'postgres';
|
||||
export type SQLDialect =
|
||||
| 'StandardSQL'
|
||||
| 'PostgreSQL'
|
||||
| 'MySQL'
|
||||
| 'MariaSQL'
|
||||
| 'MSSQL'
|
||||
| 'SQLite'
|
||||
| 'Cassandra'
|
||||
| 'PLSQL';
|
||||
|
||||
export interface ILoadOptions {
|
||||
routing?: {
|
||||
|
|
|
@ -773,9 +773,6 @@ importers:
|
|||
'@codemirror/lang-python':
|
||||
specifier: ^6.1.2
|
||||
version: 6.1.2(@codemirror/state@6.1.4)(@codemirror/view@6.5.1)(@lezer/common@1.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
|
||||
|
@ -818,6 +815,9 @@ importers:
|
|||
'@jsplumb/util':
|
||||
specifier: ^5.13.2
|
||||
version: 5.13.2
|
||||
'@n8n/codemirror-lang-sql':
|
||||
specifier: ^1.0.0
|
||||
version: 1.0.0(@codemirror/view@6.5.1)(@lezer/common@1.0.1)
|
||||
axios:
|
||||
specifier: ^0.21.1
|
||||
version: 0.21.4
|
||||
|
@ -4128,19 +4128,6 @@ packages:
|
|||
- '@lezer/common'
|
||||
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:
|
||||
|
@ -5272,6 +5259,19 @@ packages:
|
|||
dev: false
|
||||
optional: true
|
||||
|
||||
/@n8n/codemirror-lang-sql@1.0.0(@codemirror/view@6.5.1)(@lezer/common@1.0.1):
|
||||
resolution: {integrity: sha512-7bmlhaSW+f/g+IarWbif/D9bUgwW8bjCbjfW6BCGqZHXTz9UQt8fM6tQ9MNh/3sZz9LPwcnT7XSSv73Ku0rriw==}
|
||||
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
|
||||
|
||||
/@n8n_io/license-sdk@2.4.0:
|
||||
resolution: {integrity: sha512-99kuCVH4NcBi4nyn/WIpd6KSIMLk/pbBks0zr8bC65ALKj0se7/2MwC6N+WwGkG7NqH0kMdGe/7Y5KnJkMTefg==}
|
||||
engines: {node: '>=14.0.0', npm: '>=7.10.0'}
|
||||
|
|
Loading…
Reference in a new issue