Skip to content

Commit

Permalink
Merge branch 'master' into ADO-1756-add-fixed-collection-loading-and-…
Browse files Browse the repository at this point in the history
…error-states

* master:
  fix(editor): Send template id as a number in telemetry events (#8484)
  refactor(core): Replace promisify-d node calls with native promises (no-changelog) (#8464)
  fix(core): Fix stopping and retrying failed executions (#8480)
  feat: Add model parameter to OpenAI embeddings (#8481)
  fix(editor): Disable expression editor modal opening on readonly field (#8457)
  fix(core): Prevent calling internal hook email event if emailing is disabled (#8462)
  • Loading branch information
MiloradFilipovic committed Jan 30, 2024
2 parents e90d13e + 327cc8d commit 1edee97
Show file tree
Hide file tree
Showing 30 changed files with 822 additions and 639 deletions.
5 changes: 2 additions & 3 deletions .github/scripts/check-tests.mjs
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import fs from 'fs';
import { readFile } from 'fs/promises';
import path from 'path';
import util from 'util';
import { exec } from 'child_process';
import { glob } from 'glob';
import ts from 'typescript';

const readFileAsync = util.promisify(fs.readFile);
const execAsync = util.promisify(exec);

const filterAsync = async (asyncPredicate, arr) => {
Expand Down Expand Up @@ -37,7 +36,7 @@ const isAbstractMethod = (node) => {

// Function to check if a file has a function declaration, function expression, object method or class
const hasFunctionOrClass = async (filePath) => {
const fileContent = await readFileAsync(filePath, 'utf-8');
const fileContent = await readFile(filePath, 'utf-8');
const sourceFile = ts.createSourceFile(filePath, fileContent, ts.ScriptTarget.Latest, true);

let hasFunctionOrClass = false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,68 @@ import {
type INodeType,
type INodeTypeDescription,
type SupplyData,
type INodeProperties,
} from 'n8n-workflow';

import type { ClientOptions } from 'openai';
import { OpenAIEmbeddings } from 'langchain/embeddings/openai';
import { logWrapper } from '../../../utils/logWrapper';
import { getConnectionHintNoticeField } from '../../../utils/sharedFields';

const modelParameter: INodeProperties = {
displayName: 'Model',
name: 'model',
type: 'options',
description:
'The model which will generate the embeddings. <a href="https://platform.openai.com/docs/models/overview">Learn more</a>.',
typeOptions: {
loadOptions: {
routing: {
request: {
method: 'GET',
url: '={{ $parameter.options?.baseURL?.split("/").slice(-1).pop() || "v1" }}/models',
},
output: {
postReceive: [
{
type: 'rootProperty',
properties: {
property: 'data',
},
},
{
type: 'filter',
properties: {
pass: "={{ $responseItem.id.includes('embed') }}",
},
},
{
type: 'setKeyValue',
properties: {
name: '={{$responseItem.id}}',
value: '={{$responseItem.id}}',
},
},
{
type: 'sort',
properties: {
key: 'name',
},
},
],
},
},
},
},
routing: {
send: {
type: 'body',
property: 'model',
},
},
default: 'text-embedding-3-small',
};

export class EmbeddingsOpenAi implements INodeType {
description: INodeTypeDescription = {
displayName: 'Embeddings OpenAI',
Expand Down Expand Up @@ -50,6 +105,23 @@ export class EmbeddingsOpenAi implements INodeType {
outputNames: ['Embeddings'],
properties: [
getConnectionHintNoticeField([NodeConnectionType.AiVectorStore]),
{
...modelParameter,
default: 'text-embedding-ada-002',
displayOptions: {
show: {
'@version': [1],
},
},
},
{
...modelParameter,
displayOptions: {
hide: {
'@version': [1],
},
},
},
{
displayName: 'Options',
name: 'options',
Expand Down Expand Up @@ -115,6 +187,7 @@ export class EmbeddingsOpenAi implements INodeType {

const embeddings = new OpenAIEmbeddings(
{
modelName: this.getNodeParameter('model', itemIndex, 'text-embedding-3-small') as string,
openAIApiKey: credentials.apiKey as string,
...options,
},
Expand Down
137 changes: 106 additions & 31 deletions packages/cli/src/UserManagement/email/UserManagementMailer.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
import { Container, Service } from 'typedi';
import { existsSync } from 'fs';
import { readFile } from 'fs/promises';
import Handlebars from 'handlebars';
import { join as pathJoin } from 'path';
import { Container, Service } from 'typedi';
import { ApplicationError } from 'n8n-workflow';

import config from '@/config';
import type { User } from '@db/entities/User';
import type { WorkflowEntity } from '@db/entities/WorkflowEntity';
import { UserRepository } from '@db/repositories/user.repository';
import { InternalHooks } from '@/InternalHooks';
import { Logger } from '@/Logger';
import { UrlService } from '@/services/url.service';
import { InternalServerError } from '@/errors/response-errors/internal-server.error';
import { toError } from '@/utils';

import type { InviteEmailData, PasswordResetData, SendEmailResult } from './Interfaces';
import { NodeMailer } from './NodeMailer';
import { ApplicationError } from 'n8n-workflow';

type Template = HandlebarsTemplateDelegate<unknown>;
type TemplateName = 'invite' | 'passwordReset' | 'workflowShared' | 'credentialsShared';
Expand Down Expand Up @@ -39,7 +49,11 @@ export class UserManagementMailer {

private mailer: NodeMailer | undefined;

constructor() {
constructor(
private readonly userRepository: UserRepository,
private readonly logger: Logger,
private readonly urlService: UrlService,
) {
this.isEmailSetUp =
config.getEnv('userManagement.emails.mode') === 'smtp' &&
config.getEnv('userManagement.emails.smtp.host') !== '';
Expand Down Expand Up @@ -83,48 +97,109 @@ export class UserManagementMailer {
}

async notifyWorkflowShared({
recipientEmails,
workflowName,
baseUrl,
workflowId,
sharerFirstName,
sharer,
newShareeIds,
workflow,
}: {
recipientEmails: string[];
workflowName: string;
baseUrl: string;
workflowId: string;
sharerFirstName: string;
sharer: User;
newShareeIds: string[];
workflow: WorkflowEntity;
}) {
if (!this.mailer) return;

const recipients = await this.userRepository.getEmailsByIds(newShareeIds);

if (recipients.length === 0) return;

const emailRecipients = recipients.map(({ email }) => email);

const populateTemplate = await getTemplate('workflowShared', 'workflowShared.html');

const result = await this.mailer?.sendMail({
emailRecipients: recipientEmails,
subject: `${sharerFirstName} has shared an n8n workflow with you`,
body: populateTemplate({ workflowName, workflowUrl: `${baseUrl}/workflow/${workflowId}` }),
});
const baseUrl = this.urlService.getInstanceBaseUrl();

return result ?? { emailSent: false };
try {
const result = await this.mailer.sendMail({
emailRecipients,
subject: `${sharer.firstName} has shared an n8n workflow with you`,
body: populateTemplate({
workflowName: workflow.name,
workflowUrl: `${baseUrl}/workflow/${workflow.id}`,
}),
});

if (!result) return { emailSent: false };

this.logger.info('Sent workflow shared email successfully', { sharerId: sharer.id });

void Container.get(InternalHooks).onUserTransactionalEmail({
user_id: sharer.id,
message_type: 'Workflow shared',
public_api: false,
});

return result;
} catch (e) {
void Container.get(InternalHooks).onEmailFailed({
user: sharer,
message_type: 'Workflow shared',
public_api: false,
});

const error = toError(e);

throw new InternalServerError(`Please contact your administrator: ${error.message}`);
}
}

async notifyCredentialsShared({
sharerFirstName,
sharer,
newShareeIds,
credentialsName,
recipientEmails,
baseUrl,
}: {
sharerFirstName: string;
sharer: User;
newShareeIds: string[];
credentialsName: string;
recipientEmails: string[];
baseUrl: string;
}) {
if (!this.mailer) return;

const recipients = await this.userRepository.getEmailsByIds(newShareeIds);

if (recipients.length === 0) return;

const emailRecipients = recipients.map(({ email }) => email);

const populateTemplate = await getTemplate('credentialsShared', 'credentialsShared.html');

const result = await this.mailer?.sendMail({
emailRecipients: recipientEmails,
subject: `${sharerFirstName} has shared an n8n credential with you`,
body: populateTemplate({ credentialsName, credentialsListUrl: `${baseUrl}/credentials` }),
});
const baseUrl = this.urlService.getInstanceBaseUrl();

return result ?? { emailSent: false };
try {
const result = await this.mailer.sendMail({
emailRecipients,
subject: `${sharer.firstName} has shared an n8n credential with you`,
body: populateTemplate({ credentialsName, credentialsListUrl: `${baseUrl}/credentials` }),
});

if (!result) return { emailSent: false };

this.logger.info('Sent credentials shared email successfully', { sharerId: sharer.id });

void Container.get(InternalHooks).onUserTransactionalEmail({
user_id: sharer.id,
message_type: 'Credentials shared',
public_api: false,
});

return result;
} catch (e) {
void Container.get(InternalHooks).onEmailFailed({
user: sharer,
message_type: 'Credentials shared',
public_api: false,
});

const error = toError(e);

throw new InternalServerError(`Please contact your administrator: ${error.message}`);
}
}
}
5 changes: 1 addition & 4 deletions packages/cli/src/WebhookHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@
import type express from 'express';
import { Container } from 'typedi';
import get from 'lodash/get';
import stream from 'stream';
import { promisify } from 'util';
import { pipeline } from 'stream/promises';
import formidable from 'formidable';

import { BinaryDataService, NodeExecuteFunctions } from 'n8n-core';
Expand Down Expand Up @@ -65,8 +64,6 @@ import { NotFoundError } from './errors/response-errors/not-found.error';
import { InternalServerError } from './errors/response-errors/internal-server.error';
import { UnprocessableRequestError } from './errors/response-errors/unprocessable.error';

const pipeline = promisify(stream.pipeline);

export const WEBHOOK_METHODS: IHttpRequestMethods[] = [
'DELETE',
'GET',
Expand Down
4 changes: 1 addition & 3 deletions packages/cli/src/commands/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@ import { Flags, type Config } from '@oclif/core';
import path from 'path';
import { mkdir } from 'fs/promises';
import { createReadStream, createWriteStream, existsSync } from 'fs';
import stream from 'stream';
import { pipeline } from 'stream/promises';
import replaceStream from 'replacestream';
import { promisify } from 'util';
import glob from 'fast-glob';
import { sleep, jsonParse } from 'n8n-workflow';

Expand All @@ -31,7 +30,6 @@ import { BaseCommand } from './BaseCommand';

// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-var-requires
const open = require('open');
const pipeline = promisify(stream.pipeline);

export class Start extends BaseCommand {
static description = 'Starts n8n. Makes Web-UI available and starts active workflows';
Expand Down
38 changes: 4 additions & 34 deletions packages/cli/src/credentials/credentials.controller.ee.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,7 @@ import { NotFoundError } from '@/errors/response-errors/not-found.error';
import { UnauthorizedError } from '@/errors/response-errors/unauthorized.error';
import { CredentialsRepository } from '@/databases/repositories/credentials.repository';
import * as utils from '@/utils';
import { UserRepository } from '@/databases/repositories/user.repository';
import { UserManagementMailer } from '@/UserManagement/email';
import { UrlService } from '@/services/url.service';
import { Logger } from '@/Logger';
import { InternalServerError } from '@/errors/response-errors/internal-server.error';

export const EECredentialsController = express.Router();

Expand Down Expand Up @@ -190,36 +186,10 @@ EECredentialsController.put(
sharees_removed: amountRemoved,
});

const recipients = await Container.get(UserRepository).getEmailsByIds(newShareeIds);

if (recipients.length === 0) return;

try {
await Container.get(UserManagementMailer).notifyCredentialsShared({
sharerFirstName: req.user.firstName,
credentialsName: credential.name,
recipientEmails: recipients.map(({ email }) => email),
baseUrl: Container.get(UrlService).getInstanceBaseUrl(),
});
} catch (error) {
void Container.get(InternalHooks).onEmailFailed({
user: req.user,
message_type: 'Credentials shared',
public_api: false,
});
if (error instanceof Error) {
throw new InternalServerError(`Please contact your administrator: ${error.message}`);
}
}

Container.get(Logger).info('Sent credentials shared email successfully', {
sharerId: req.user.id,
});

void Container.get(InternalHooks).onUserTransactionalEmail({
user_id: req.user.id,
message_type: 'Credentials shared',
public_api: false,
await Container.get(UserManagementMailer).notifyCredentialsShared({
sharer: req.user,
newShareeIds,
credentialsName: credential.name,
});
}),
);
Loading

0 comments on commit 1edee97

Please sign in to comment.