Skip to content

Commit

Permalink
Merge branch 'master' into ado-251-data-transformation-fixes
Browse files Browse the repository at this point in the history
* master: (26 commits)
  feat(editor): Completions for extensions in expression editor (#5130)
  feat(core): Set custom Cache-Control headers for static assets (#5322)
  fix(editor): Set max width for executions list (#5302)
  refactor(Code Node): Remove disallowed syntax checks (#5332)
  fix(Linear Node): Fix pagination issue for get all issues (#5324)
  refactor(editor): Fix duplicate NodeView keys when navigating between routes (no-changelog) (#5325)
  fix(Mailchimp Trigger Node): Fix webhook recreation (#5328)
  fix(Schedule Trigger Node): Change scheduler behaviour for intervals days and hours (#5133)
  fix(core): Fix oauth2 client credentials not always working (#5327)
  feat(Salesforce Node): Add HasOptedOutOfEmail field to lead resource (#5235)
  fix: Update app screenshot in README to match latest design (no-changelog) (#5314)
  fix(editor): Stop unsaved changes popup display when navigating away from an untouched workflow (#5259)
  feat(core): Simplify pagination in declarative node design (#5161)
  fix(core): Fix value resolution in declarative node design (#5217)
  feat(YouTube Node): Switch upload operation over to streaming and resumable uploads api (#5320)
  fix(YouTube Node): Update description for channel id (no-changelog) (#5321)
  fix(core): Revert back to console logging in config (no-changelog) (#5319)
  fix: Add paired item to the most used nodes (#5220)
  feat(Write Binary File Node): Stream binary data for writes (#5306)
  feat(editor): Roll out schema view (#5310)
  ...

# Conflicts:
#	packages/cli/BREAKING-CHANGES.md
#	packages/editor-ui/src/components/ExpressionEditorModal/ExpressionEditorModalInput.vue
#	packages/editor-ui/src/components/InlineExpressionEditor/InlineExpressionEditorInput.vue
#	packages/editor-ui/src/mixins/completionManager.ts
#	packages/editor-ui/src/mixins/expressionManager.ts
#	packages/editor-ui/src/plugins/codemirror/completions/__tests__/completions.test.ts
#	packages/editor-ui/src/plugins/codemirror/completions/datatype.completions.ts
#	packages/editor-ui/src/plugins/codemirror/completions/dollar.completions.ts
#	packages/editor-ui/src/plugins/codemirror/completions/types.ts
#	packages/editor-ui/src/plugins/codemirror/completions/utils.ts
#	packages/workflow/src/Expression.ts
#	packages/workflow/src/Extensions/ArrayExtensions.ts
#	packages/workflow/src/Extensions/DateExtensions.ts
#	packages/workflow/src/Extensions/Extensions.ts
#	packages/workflow/src/Extensions/NumberExtensions.ts
#	packages/workflow/src/Extensions/ObjectExtensions.ts
#	packages/workflow/src/Extensions/StringExtensions.ts
#	packages/workflow/src/Extensions/index.ts
#	packages/workflow/src/index.ts
#	packages/workflow/test/ExpressionExtensions/ArrayExtensions.test.ts
  • Loading branch information
MiloradFilipovic committed Feb 2, 2023
2 parents 938d025 + 6d811f0 commit 965ca55
Show file tree
Hide file tree
Showing 84 changed files with 1,508 additions and 729 deletions.
Binary file modified assets/n8n-screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 7 additions & 3 deletions cypress/e2e/11-inline-expression-editor.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,16 @@ describe('Inline expression editor', () => {

it('should resolve object resolvables', () => {
WorkflowPage.getters.inlineExpressionEditorInput().type('{{');
WorkflowPage.getters.inlineExpressionEditorInput().type('{{} a: 1');
WorkflowPage.getters.inlineExpressionEditorOutput().contains(/^\[Object: \{"a":1\}\]$/);
WorkflowPage.getters
.inlineExpressionEditorInput()
.type('{ a: 1 }', { parseSpecialCharSequences: false });
WorkflowPage.getters.inlineExpressionEditorOutput().contains(/^\[Object: \{"a": 1\}\]$/);
WorkflowPage.getters.inlineExpressionEditorInput().clear();

WorkflowPage.getters.inlineExpressionEditorInput().type('{{');
WorkflowPage.getters.inlineExpressionEditorInput().type('{{} a: 1 }.a{del}{del}');
WorkflowPage.getters
.inlineExpressionEditorInput()
.type('{ a: 1 }.a', { parseSpecialCharSequences: false });
WorkflowPage.getters.inlineExpressionEditorOutput().contains(/^1$/);
});

Expand Down
30 changes: 13 additions & 17 deletions cypress/e2e/9-expression-editor-modal.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,49 +17,45 @@ describe('Expression editor modal', () => {
});

it('should resolve primitive resolvables', () => {
WorkflowPage.getters.expressionModalInput().type('{{');
WorkflowPage.getters.expressionModalInput().type('1 + 2');
WorkflowPage.getters.expressionModalInput().type('{{ 1 + 2');
WorkflowPage.getters.expressionModalOutput().contains(/^3$/);
WorkflowPage.getters.expressionModalInput().clear();

WorkflowPage.getters.expressionModalInput().type('{{');
WorkflowPage.getters.expressionModalInput().type('"ab" + "cd"');
WorkflowPage.getters.expressionModalInput().type('{{ "ab" + "cd"');
WorkflowPage.getters.expressionModalOutput().contains(/^abcd$/);

WorkflowPage.getters.expressionModalInput().clear();

WorkflowPage.getters.expressionModalInput().type('{{');
WorkflowPage.getters.expressionModalInput().type('true && false');
WorkflowPage.getters.expressionModalInput().type('{{ true && false');
WorkflowPage.getters.expressionModalOutput().contains(/^false$/);
});

it('should resolve object resolvables', () => {
WorkflowPage.getters.expressionModalInput().type('{{');
WorkflowPage.getters.expressionModalInput().type('{{} a: 1');
WorkflowPage.getters.expressionModalOutput().contains(/^\[Object: \{"a":1\}\]$/);
WorkflowPage.getters
.expressionModalInput()
.type('{{ { a : 1 }', { parseSpecialCharSequences: false });
WorkflowPage.getters.expressionModalOutput().contains(/^\[Object: \{"a": 1\}\]$/);

WorkflowPage.getters.expressionModalInput().clear();

WorkflowPage.getters.expressionModalInput().type('{{');
WorkflowPage.getters.expressionModalInput().type('{{} a: 1 }.a{del}{del}');
WorkflowPage.getters
.expressionModalInput()
.type('{{ { a : 1 }.a', { parseSpecialCharSequences: false });
WorkflowPage.getters.expressionModalOutput().contains(/^1$/);
});

it('should resolve array resolvables', () => {
WorkflowPage.getters.expressionModalInput().type('{{');
WorkflowPage.getters.expressionModalInput().type('[1, 2, 3]');
WorkflowPage.getters.expressionModalInput().type('{{ [1, 2, 3]');
WorkflowPage.getters.expressionModalOutput().contains(/^\[Array: \[1,2,3\]\]$/);

WorkflowPage.getters.expressionModalInput().clear();

WorkflowPage.getters.expressionModalInput().type('{{');
WorkflowPage.getters.expressionModalInput().type('[1, 2, 3][0]');
WorkflowPage.getters.expressionModalInput().type('{{ [1, 2, 3][0]');
WorkflowPage.getters.expressionModalOutput().contains(/^1$/);
});

it('should resolve $parameter[]', () => {
WorkflowPage.getters.expressionModalInput().type('{{');
WorkflowPage.getters.expressionModalInput().type('$parameter["operation"]');
WorkflowPage.getters.expressionModalInput().type('{{ $parameter["operation"]');
WorkflowPage.getters.expressionModalOutput().contains(/^get$/);
});
});
4 changes: 2 additions & 2 deletions packages/cli/BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ This list shows all the versions which include breaking changes and how to upgra

### What changed?

In expressions, `DateTime.fromHTTP()`, `DateTime.fromISO()` and `DateTime.fromJSDate()` require an argument. Before, they all resolved `null` when called without an argument; now, they throw an error when called without an argument.
Invalid Luxon datetimes no longer resolve to `null`. Now they throw the error `invalid DateTime`.

### When is action necessary?

If you are relying on the above behavior, review your workflow to ensure you are passing in the required number of arguments.
If you are relying on the above behavior, review your workflow to ensure you handle invalid Luxon datetimes.

## 0.202.0

Expand Down
19 changes: 17 additions & 2 deletions packages/cli/src/Server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { createHmac } from 'crypto';
import { promisify } from 'util';
import cookieParser from 'cookie-parser';
import express from 'express';
import type { ServeStaticOptions } from 'serve-static';
import type { FindManyOptions } from 'typeorm';
import { In } from 'typeorm';
import type { AxiosRequestConfig } from 'axios';
Expand Down Expand Up @@ -1416,16 +1417,30 @@ class Server extends AbstractServer {
);
}

const staticOptions: ServeStaticOptions = {
cacheControl: false,
setHeaders: (res: express.Response, path: string) => {
const isIndex = path === pathJoin(GENERATED_STATIC_DIR, 'index.html');
const cacheControl = isIndex
? 'no-cache, no-store, must-revalidate'
: 'max-age=86400, immutable';
res.header('Cache-Control', cacheControl);
},
};
if (!config.getEnv('endpoints.disableUi')) {
this.app.use('/', express.static(GENERATED_STATIC_DIR), express.static(EDITOR_UI_DIST_DIR));
this.app.use(
'/',
express.static(GENERATED_STATIC_DIR, staticOptions),
express.static(EDITOR_UI_DIST_DIR, staticOptions),
);

const startTime = new Date().toUTCString();
this.app.use('/index.html', (req, res, next) => {
res.setHeader('Last-Modified', startTime);
next();
});
} else {
this.app.use('/', express.static(GENERATED_STATIC_DIR));
this.app.use('/', express.static(GENERATED_STATIC_DIR, staticOptions));
}
}
}
Expand Down
5 changes: 2 additions & 3 deletions packages/cli/src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import dotenv from 'dotenv';
import { tmpdir } from 'os';
import { mkdtempSync, readFileSync } from 'fs';
import { join } from 'path';
import { LoggerProxy as Logger } from 'n8n-workflow';
import { schema } from './schema';
import { inTest, inE2ETests } from '@/constants';

Expand Down Expand Up @@ -40,7 +39,7 @@ if (!inE2ETests && !inTest) {
const { N8N_CONFIG_FILES } = process.env;
if (N8N_CONFIG_FILES !== undefined) {
const configFiles = N8N_CONFIG_FILES.split(',');
Logger.debug('Loading config overwrites', configFiles);
console.debug('Loading config overwrites', configFiles);
config.loadFile(configFiles);
}

Expand All @@ -61,7 +60,7 @@ if (!inE2ETests && !inTest) {
}
throw error;
}
Logger.debug('Loading config overwrite', { fileName });
console.debug('Loading config overwrite', { fileName });
acc[key] = value;
}
}
Expand Down
15 changes: 13 additions & 2 deletions packages/cli/src/executions/executions.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,8 +223,19 @@ export class ExecutionsService {
});
}

let query = Db.collections.Execution.createQueryBuilder()
.select()
// Omit `data` from the Execution since it is the largest and not necesary for the list.
let query = Db.collections.Execution.createQueryBuilder('execution')
.select([
'execution.id',
'execution.finished',
'execution.mode',
'execution.retryOf',
'execution.retrySuccessId',
'execution.waitTill',
'execution.startedAt',
'execution.stoppedAt',
'execution.workflowData',
])
.orderBy('id', 'DESC')
.take(limit)
.where(findWhere);
Expand Down
6 changes: 1 addition & 5 deletions packages/cli/src/middlewares/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,11 +126,7 @@ export const setupAuthMiddlewares = (
`/${restEndpoint}/ldap/sync`,
`/${restEndpoint}/ldap/test-connection`,
];
const getRestrictedUrls = [
`/${restEndpoint}/users`,
`/${restEndpoint}/ldap/sync`,
`/${restEndpoint}/ldap/config`,
];
const getRestrictedUrls = [`/${restEndpoint}/ldap/sync`, `/${restEndpoint}/ldap/config`];
const putRestrictedUrls = [`/${restEndpoint}/ldap/config`];
const trimmedUrl = req.url.endsWith('/') ? req.url.slice(0, -1) : req.url;
if (
Expand Down
8 changes: 6 additions & 2 deletions packages/cli/src/workflows/workflows.services.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { validate as jsonSchemaValidate } from 'jsonschema';
import type { INode, IPinData, JsonObject } from 'n8n-workflow';
import { jsonParse, LoggerProxy, Workflow } from 'n8n-workflow';
import { NodeApiError, jsonParse, LoggerProxy, Workflow } from 'n8n-workflow';
import type { FindOptionsWhere } from 'typeorm';
import { In } from 'typeorm';
import pick from 'lodash.pick';
Expand Down Expand Up @@ -336,8 +336,12 @@ export class WorkflowsService {
// Also set it in the returned data
updatedWorkflow.active = false;

let message;
if (error instanceof NodeApiError) message = error.description;
message = message ?? (error as Error).message;

// Now return the original error for UI to display
throw new ResponseHelper.BadRequestError((error as Error).message);
throw new ResponseHelper.BadRequestError(message);
}
}

Expand Down
5 changes: 4 additions & 1 deletion packages/core/src/NodeExecuteFunctions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1029,7 +1029,10 @@ export async function requestOAuth2(
let oauthTokenData = credentials.oauthTokenData as clientOAuth2.Data;

// if it's the first time using the credentials, get the access token and save it into the DB.
if (credentials.grantType === OAuth2GrantType.clientCredentials && oauthTokenData === undefined) {
if (
credentials.grantType === OAuth2GrantType.clientCredentials &&
(oauthTokenData === undefined || Object.keys(oauthTokenData).length === 0)
) {
const { data } = await getClientCredentialsToken(oAuthClient, credentials);

// Find the credentials
Expand Down
3 changes: 3 additions & 0 deletions packages/editor-ui/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -241,9 +241,12 @@ export default mixins(showMessage, userHelpers, restApi, historyHelper).extend({
}
.content {
display: flex;
grid-area: content;
overflow: auto;
height: 100vh;
width: 100%;
justify-content: center;
}
.header {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,10 @@ export const jsonFieldCompletions = (Vue as CodeNodeEditorMixin).extend({
jsonOutput: IDataObject,
matcher: string, // e.g. `$input.first().json` or `x` (user-defined variable)
) {
if (preCursor.text.endsWith('.json[') || preCursor.text.endsWith(`${matcher}[`)) {
if (
/\.json\[/.test(preCursor.text) ||
new RegExp(`(${escape(matcher)})\\[`).test(preCursor.text)
) {
const options: Completion[] = Object.keys(jsonOutput)
.map((field) => `${matcher}['${field}']`)
.map((label) => ({
Expand All @@ -248,7 +251,10 @@ export const jsonFieldCompletions = (Vue as CodeNodeEditorMixin).extend({
};
}

if (preCursor.text.endsWith('.json.') || preCursor.text.endsWith(`${matcher}.`)) {
if (
/\.json\./.test(preCursor.text) ||
new RegExp(`(${escape(matcher)})\.`).test(preCursor.text)
) {
const options: Completion[] = Object.keys(jsonOutput)
.filter(isAllowedInDotNotation)
.map((field) => `${matcher}.${field}`)
Expand Down
2 changes: 2 additions & 0 deletions packages/editor-ui/src/components/ExecutionsList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -927,6 +927,8 @@ export default mixins(externalHooks, genericHelpers, executionHelpers, restApi,
grid-template-rows: 1fr 0;
position: relative;
height: 100%;
width: 100%;
max-width: 1280px;
}
.execList {
Expand Down
49 changes: 24 additions & 25 deletions packages/editor-ui/src/components/ExecutionsView/ExecutionsList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
<script lang="ts">
import ExecutionsSidebar from '@/components/ExecutionsView/ExecutionsSidebar.vue';
import {
MAIN_HEADER_TABS,
MODAL_CANCEL,
MODAL_CLOSE,
MODAL_CONFIRMED,
Expand Down Expand Up @@ -121,36 +122,34 @@ export default mixins(
},
},
async beforeRouteLeave(to, from, next) {
const nextTab = getNodeViewTab(to);
// When leaving for a page that's not a workflow view tab, ask to save changes
if (!nextTab) {
const result = this.uiStore.stateIsDirty;
if (result) {
const confirmModal = await this.confirmModal(
this.$locale.baseText('generic.unsavedWork.confirmMessage.message'),
this.$locale.baseText('generic.unsavedWork.confirmMessage.headline'),
'warning',
this.$locale.baseText('generic.unsavedWork.confirmMessage.confirmButtonText'),
this.$locale.baseText('generic.unsavedWork.confirmMessage.cancelButtonText'),
true,
);
if (getNodeViewTab(to) === MAIN_HEADER_TABS.WORKFLOW) {
next();
return;
}
if (this.uiStore.stateIsDirty) {
const confirmModal = await this.confirmModal(
this.$locale.baseText('generic.unsavedWork.confirmMessage.message'),
this.$locale.baseText('generic.unsavedWork.confirmMessage.headline'),
'warning',
this.$locale.baseText('generic.unsavedWork.confirmMessage.confirmButtonText'),
this.$locale.baseText('generic.unsavedWork.confirmMessage.cancelButtonText'),
true,
);
if (confirmModal === MODAL_CONFIRMED) {
const saved = await this.saveCurrentWorkflow({}, false);
if (saved) this.settingsStore.fetchPromptsData();
this.uiStore.stateIsDirty = false;
next();
} else if (confirmModal === MODAL_CANCEL) {
this.uiStore.stateIsDirty = false;
next();
} else if (confirmModal === MODAL_CLOSE) {
next(false);
if (confirmModal === MODAL_CONFIRMED) {
const saved = await this.saveCurrentWorkflow({}, false);
if (saved) {
await this.settingsStore.fetchPromptsData();
}
} else {
this.uiStore.stateIsDirty = false;
next();
} else if (confirmModal === MODAL_CANCEL) {
this.uiStore.stateIsDirty = false;
next();
}
} else {
next();
}
next();
},
async mounted() {
this.loading = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,12 @@ export default mixins(expressionManager, completionManager, workflowHelpers).ext
highlighter.removeColor(this.editor, this.plaintextSegments);
highlighter.addColor(this.editor, this.resolvableSegments);
try {
this.trackCompletion(viewUpdate, this.path);
} catch {}
setTimeout(() => this.editor?.focus()); // prevent blur on paste
setTimeout(() => {
this.editor?.focus(); // prevent blur on paste
try {
this.trackCompletion(viewUpdate, this.path);
} catch {}
});
this.$emit('change', {
value: this.unresolvedExpression,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,11 @@ export default mixins(completionManager, expressionManager, workflowHelpers).ext
highlighter.removeColor(this.editor, this.plaintextSegments);
highlighter.addColor(this.editor, this.resolvableSegments);
try {
this.trackCompletion(viewUpdate, this.path);
} catch {}
setTimeout(() => {
try {
this.trackCompletion(viewUpdate, this.path);
} catch {}
});
this.$emit('change', {
value: this.unresolvedExpression,
Expand Down
2 changes: 2 additions & 0 deletions packages/editor-ui/src/components/ParameterInputFull.vue
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,8 @@ export default mixins(showMessage).extend({
prevValue.length > 1
) {
updatedValue = `${prevValue} ${data}`;
} else if (prevValue && ['string', 'json'].includes(this.parameter.type)) {
updatedValue = `=${prevValue} ${data}`;
} else {
updatedValue = `=${data}`;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/editor-ui/src/components/RunData.vue
Original file line number Diff line number Diff line change
Expand Up @@ -657,7 +657,7 @@ export default mixins(externalHooks, genericHelpers, nodeHelpers, pinData).exten
defaults.push({ label: this.$locale.baseText('runData.binary'), value: 'binary' });
}
if (this.isPaneTypeInput && window.posthog?.isFeatureEnabled?.('schema-view')) {
if (this.isPaneTypeInput) {
defaults.unshift({ label: this.$locale.baseText('runData.schema'), value: 'schema' });
}
Expand Down
Loading

0 comments on commit 965ca55

Please sign in to comment.