mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-11 12:57:29 -08:00
refactor(MQTT Node): Refactor, fix duplicate triggers, and add Unit tests (#9847)
Co-authored-by: Elias Meire <elias@meire.dev>
This commit is contained in:
parent
e51de9d391
commit
164ec72c0d
53
packages/nodes-base/nodes/MQTT/GenericFunctions.ts
Normal file
53
packages/nodes-base/nodes/MQTT/GenericFunctions.ts
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
import { connectAsync, type IClientOptions, type MqttClient } from 'mqtt';
|
||||||
|
import { randomString } from 'n8n-workflow';
|
||||||
|
import { formatPrivateKey } from '@utils/utilities';
|
||||||
|
|
||||||
|
interface BaseMqttCredential {
|
||||||
|
protocol: 'mqtt' | 'mqtts' | 'ws';
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
clean: boolean;
|
||||||
|
clientId: string;
|
||||||
|
passwordless?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
type NonSslMqttCredential = BaseMqttCredential & {
|
||||||
|
ssl: false;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SslMqttCredential = BaseMqttCredential & {
|
||||||
|
ssl: true;
|
||||||
|
ca: string;
|
||||||
|
cert: string;
|
||||||
|
key: string;
|
||||||
|
rejectUnauthorized?: boolean;
|
||||||
|
};
|
||||||
|
export type MqttCredential = NonSslMqttCredential | SslMqttCredential;
|
||||||
|
|
||||||
|
export const createClient = async (credentials: MqttCredential): Promise<MqttClient> => {
|
||||||
|
const { protocol, host, port, clean, clientId, username, password } = credentials;
|
||||||
|
|
||||||
|
const clientOptions: IClientOptions = {
|
||||||
|
protocol,
|
||||||
|
host,
|
||||||
|
port,
|
||||||
|
clean,
|
||||||
|
clientId: clientId || `mqttjs_${randomString(8).toLowerCase()}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (username && password) {
|
||||||
|
clientOptions.username = username;
|
||||||
|
clientOptions.password = password;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (credentials.ssl) {
|
||||||
|
clientOptions.ca = formatPrivateKey(credentials.ca);
|
||||||
|
clientOptions.cert = formatPrivateKey(credentials.cert);
|
||||||
|
clientOptions.key = formatPrivateKey(credentials.key);
|
||||||
|
clientOptions.rejectUnauthorized = credentials.rejectUnauthorized;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await connectAsync(clientOptions);
|
||||||
|
};
|
|
@ -1,6 +1,6 @@
|
||||||
|
import type { IClientPublishOptions } from 'mqtt';
|
||||||
import type {
|
import type {
|
||||||
IExecuteFunctions,
|
IExecuteFunctions,
|
||||||
ICredentialDataDecryptedObject,
|
|
||||||
ICredentialsDecrypted,
|
ICredentialsDecrypted,
|
||||||
ICredentialTestFunctions,
|
ICredentialTestFunctions,
|
||||||
INodeCredentialTestResult,
|
INodeCredentialTestResult,
|
||||||
|
@ -8,10 +8,10 @@ import type {
|
||||||
INodeType,
|
INodeType,
|
||||||
INodeTypeDescription,
|
INodeTypeDescription,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { randomString } from 'n8n-workflow';
|
|
||||||
|
|
||||||
import * as mqtt from 'mqtt';
|
import { createClient, type MqttCredential } from './GenericFunctions';
|
||||||
import { formatPrivateKey } from '@utils/utilities';
|
|
||||||
|
type PublishOption = Pick<IClientPublishOptions, 'qos' | 'retain'>;
|
||||||
|
|
||||||
export class Mqtt implements INodeType {
|
export class Mqtt implements INodeType {
|
||||||
description: INodeTypeDescription = {
|
description: INodeTypeDescription = {
|
||||||
|
@ -110,63 +110,11 @@ export class Mqtt implements INodeType {
|
||||||
this: ICredentialTestFunctions,
|
this: ICredentialTestFunctions,
|
||||||
credential: ICredentialsDecrypted,
|
credential: ICredentialsDecrypted,
|
||||||
): Promise<INodeCredentialTestResult> {
|
): Promise<INodeCredentialTestResult> {
|
||||||
const credentials = credential.data as ICredentialDataDecryptedObject;
|
const credentials = credential.data as unknown as MqttCredential;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const protocol = (credentials.protocol as string) || 'mqtt';
|
const client = await createClient(credentials);
|
||||||
const host = credentials.host as string;
|
client.end();
|
||||||
const brokerUrl = `${protocol}://${host}`;
|
|
||||||
const port = (credentials.port as number) || 1883;
|
|
||||||
const clientId =
|
|
||||||
(credentials.clientId as string) || `mqttjs_${randomString(8).toLowerCase()}`;
|
|
||||||
const clean = credentials.clean as boolean;
|
|
||||||
const ssl = credentials.ssl as boolean;
|
|
||||||
const ca = formatPrivateKey(credentials.ca as string);
|
|
||||||
const cert = formatPrivateKey(credentials.cert as string);
|
|
||||||
const key = formatPrivateKey(credentials.key as string);
|
|
||||||
const rejectUnauthorized = credentials.rejectUnauthorized as boolean;
|
|
||||||
|
|
||||||
let client: mqtt.MqttClient;
|
|
||||||
|
|
||||||
if (!ssl) {
|
|
||||||
const clientOptions: mqtt.IClientOptions = {
|
|
||||||
port,
|
|
||||||
clean,
|
|
||||||
clientId,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (credentials.username && credentials.password) {
|
|
||||||
clientOptions.username = credentials.username as string;
|
|
||||||
clientOptions.password = credentials.password as string;
|
|
||||||
}
|
|
||||||
client = mqtt.connect(brokerUrl, clientOptions);
|
|
||||||
} else {
|
|
||||||
const clientOptions: mqtt.IClientOptions = {
|
|
||||||
port,
|
|
||||||
clean,
|
|
||||||
clientId,
|
|
||||||
ca,
|
|
||||||
cert,
|
|
||||||
key,
|
|
||||||
rejectUnauthorized,
|
|
||||||
};
|
|
||||||
if (credentials.username && credentials.password) {
|
|
||||||
clientOptions.username = credentials.username as string;
|
|
||||||
clientOptions.password = credentials.password as string;
|
|
||||||
}
|
|
||||||
|
|
||||||
client = mqtt.connect(brokerUrl, clientOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
client.on('connect', (test) => {
|
|
||||||
resolve(test);
|
|
||||||
client.end();
|
|
||||||
});
|
|
||||||
client.on('error', (error) => {
|
|
||||||
client.end();
|
|
||||||
reject(error);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
status: 'Error',
|
status: 'Error',
|
||||||
|
@ -182,87 +130,27 @@ export class Mqtt implements INodeType {
|
||||||
};
|
};
|
||||||
|
|
||||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||||
|
const credentials = (await this.getCredentials('mqtt')) as unknown as MqttCredential;
|
||||||
|
const client = await createClient(credentials);
|
||||||
|
|
||||||
|
const publishPromises = [];
|
||||||
const items = this.getInputData();
|
const items = this.getInputData();
|
||||||
const length = items.length;
|
for (let i = 0; i < items.length; i++) {
|
||||||
const credentials = await this.getCredentials('mqtt');
|
const topic = this.getNodeParameter('topic', i) as string;
|
||||||
|
const options = this.getNodeParameter('options', i) as unknown as PublishOption;
|
||||||
const protocol = (credentials.protocol as string) || 'mqtt';
|
const sendInputData = this.getNodeParameter('sendInputData', i) as boolean;
|
||||||
const host = credentials.host as string;
|
const message = sendInputData
|
||||||
const brokerUrl = `${protocol}://${host}`;
|
? JSON.stringify(items[i].json)
|
||||||
const port = (credentials.port as number) || 1883;
|
: (this.getNodeParameter('message', i) as string);
|
||||||
const clientId = (credentials.clientId as string) || `mqttjs_${randomString(8).toLowerCase()}`;
|
publishPromises.push(client.publishAsync(topic, message, options));
|
||||||
const clean = credentials.clean as boolean;
|
|
||||||
const ssl = credentials.ssl as boolean;
|
|
||||||
const ca = credentials.ca as string;
|
|
||||||
const cert = credentials.cert as string;
|
|
||||||
const key = credentials.key as string;
|
|
||||||
const rejectUnauthorized = credentials.rejectUnauthorized as boolean;
|
|
||||||
|
|
||||||
let client: mqtt.MqttClient;
|
|
||||||
|
|
||||||
if (!ssl) {
|
|
||||||
const clientOptions: mqtt.IClientOptions = {
|
|
||||||
port,
|
|
||||||
clean,
|
|
||||||
clientId,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (credentials.username && credentials.password) {
|
|
||||||
clientOptions.username = credentials.username as string;
|
|
||||||
clientOptions.password = credentials.password as string;
|
|
||||||
}
|
|
||||||
|
|
||||||
client = mqtt.connect(brokerUrl, clientOptions);
|
|
||||||
} else {
|
|
||||||
const clientOptions: mqtt.IClientOptions = {
|
|
||||||
port,
|
|
||||||
clean,
|
|
||||||
clientId,
|
|
||||||
ca,
|
|
||||||
cert,
|
|
||||||
key,
|
|
||||||
rejectUnauthorized,
|
|
||||||
};
|
|
||||||
if (credentials.username && credentials.password) {
|
|
||||||
clientOptions.username = credentials.username as string;
|
|
||||||
clientOptions.password = credentials.password as string;
|
|
||||||
}
|
|
||||||
|
|
||||||
client = mqtt.connect(brokerUrl, clientOptions);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const sendInputData = this.getNodeParameter('sendInputData', 0) as boolean;
|
await Promise.all(publishPromises);
|
||||||
|
|
||||||
const data = await new Promise((resolve, reject) => {
|
// wait for the in-flight messages to be acked.
|
||||||
client.on('connect', () => {
|
// needed for messages with QoS 1 & 2
|
||||||
for (let i = 0; i < length; i++) {
|
await client.endAsync();
|
||||||
let message;
|
|
||||||
const topic = this.getNodeParameter('topic', i) as string;
|
|
||||||
const options = this.getNodeParameter('options', i);
|
|
||||||
|
|
||||||
try {
|
return [items];
|
||||||
if (sendInputData) {
|
|
||||||
message = JSON.stringify(items[i].json);
|
|
||||||
} else {
|
|
||||||
message = this.getNodeParameter('message', i) as string;
|
|
||||||
}
|
|
||||||
client.publish(topic, message, options);
|
|
||||||
} catch (e) {
|
|
||||||
reject(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//wait for the in-flight messages to be acked.
|
|
||||||
//needed for messages with QoS 1 & 2
|
|
||||||
client.end(false, {}, () => {
|
|
||||||
resolve([items]);
|
|
||||||
});
|
|
||||||
|
|
||||||
client.on('error', (e) => {
|
|
||||||
reject(e);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return data as INodeExecutionData[][];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,22 @@
|
||||||
|
import type { ISubscriptionMap } from 'mqtt';
|
||||||
|
import type { QoS } from 'mqtt-packet';
|
||||||
import type {
|
import type {
|
||||||
ITriggerFunctions,
|
ITriggerFunctions,
|
||||||
IDataObject,
|
IDataObject,
|
||||||
INodeType,
|
INodeType,
|
||||||
INodeTypeDescription,
|
INodeTypeDescription,
|
||||||
ITriggerResponse,
|
ITriggerResponse,
|
||||||
IDeferredPromise,
|
|
||||||
IRun,
|
IRun,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { NodeOperationError, randomString } from 'n8n-workflow';
|
import { NodeOperationError } from 'n8n-workflow';
|
||||||
|
|
||||||
import * as mqtt from 'mqtt';
|
import { createClient, type MqttCredential } from './GenericFunctions';
|
||||||
import { formatPrivateKey } from '@utils/utilities';
|
|
||||||
|
interface Options {
|
||||||
|
jsonParseBody: boolean;
|
||||||
|
onlyMessage: boolean;
|
||||||
|
parallelProcessing: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export class MqttTrigger implements INodeType {
|
export class MqttTrigger implements INodeType {
|
||||||
description: INodeTypeDescription = {
|
description: INodeTypeDescription = {
|
||||||
|
@ -87,120 +93,64 @@ export class MqttTrigger implements INodeType {
|
||||||
};
|
};
|
||||||
|
|
||||||
async trigger(this: ITriggerFunctions): Promise<ITriggerResponse> {
|
async trigger(this: ITriggerFunctions): Promise<ITriggerResponse> {
|
||||||
const credentials = await this.getCredentials('mqtt');
|
|
||||||
|
|
||||||
const topics = (this.getNodeParameter('topics') as string).split(',');
|
const topics = (this.getNodeParameter('topics') as string).split(',');
|
||||||
|
if (!topics?.length) {
|
||||||
const topicsQoS: IDataObject = {};
|
|
||||||
|
|
||||||
for (const data of topics) {
|
|
||||||
const [topic, qos] = data.split(':');
|
|
||||||
topicsQoS[topic] = qos ? { qos: parseInt(qos, 10) } : { qos: 0 };
|
|
||||||
}
|
|
||||||
|
|
||||||
const options = this.getNodeParameter('options') as IDataObject;
|
|
||||||
const parallelProcessing = this.getNodeParameter('options.parallelProcessing', true) as boolean;
|
|
||||||
|
|
||||||
if (!topics) {
|
|
||||||
throw new NodeOperationError(this.getNode(), 'Topics are mandatory!');
|
throw new NodeOperationError(this.getNode(), 'Topics are mandatory!');
|
||||||
}
|
}
|
||||||
|
|
||||||
const protocol = (credentials.protocol as string) || 'mqtt';
|
const topicsQoS: ISubscriptionMap = {};
|
||||||
const host = credentials.host as string;
|
for (const data of topics) {
|
||||||
const brokerUrl = `${protocol}://${host}`;
|
const [topic, qosString] = data.split(':');
|
||||||
const port = (credentials.port as number) || 1883;
|
let qos = qosString ? parseInt(qosString, 10) : 0;
|
||||||
const clientId = (credentials.clientId as string) || `mqttjs_${randomString(8).toLowerCase()}`;
|
if (qos < 0 || qos > 2) qos = 0;
|
||||||
const clean = credentials.clean as boolean;
|
topicsQoS[topic] = { qos: qos as QoS };
|
||||||
const ssl = credentials.ssl as boolean;
|
|
||||||
const ca = formatPrivateKey(credentials.ca as string);
|
|
||||||
const cert = formatPrivateKey(credentials.cert as string);
|
|
||||||
const key = formatPrivateKey(credentials.key as string);
|
|
||||||
const rejectUnauthorized = credentials.rejectUnauthorized as boolean;
|
|
||||||
|
|
||||||
let client: mqtt.MqttClient;
|
|
||||||
|
|
||||||
if (!ssl) {
|
|
||||||
const clientOptions: mqtt.IClientOptions = {
|
|
||||||
port,
|
|
||||||
clean,
|
|
||||||
clientId,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (credentials.username && credentials.password) {
|
|
||||||
clientOptions.username = credentials.username as string;
|
|
||||||
clientOptions.password = credentials.password as string;
|
|
||||||
}
|
|
||||||
|
|
||||||
client = mqtt.connect(brokerUrl, clientOptions);
|
|
||||||
} else {
|
|
||||||
const clientOptions: mqtt.IClientOptions = {
|
|
||||||
port,
|
|
||||||
clean,
|
|
||||||
clientId,
|
|
||||||
ca,
|
|
||||||
cert,
|
|
||||||
key,
|
|
||||||
rejectUnauthorized,
|
|
||||||
};
|
|
||||||
if (credentials.username && credentials.password) {
|
|
||||||
clientOptions.username = credentials.username as string;
|
|
||||||
clientOptions.password = credentials.password as string;
|
|
||||||
}
|
|
||||||
|
|
||||||
client = mqtt.connect(brokerUrl, clientOptions);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const manualTriggerFunction = async () => {
|
const options = this.getNodeParameter('options') as Options;
|
||||||
await new Promise((resolve, reject) => {
|
const credentials = (await this.getCredentials('mqtt')) as unknown as MqttCredential;
|
||||||
client.on('connect', () => {
|
const client = await createClient(credentials);
|
||||||
client.subscribe(topicsQoS as mqtt.ISubscriptionMap, (error, _granted) => {
|
|
||||||
if (error) {
|
|
||||||
reject(error);
|
|
||||||
}
|
|
||||||
client.on('message', async (topic: string, message: Buffer | string) => {
|
|
||||||
let result: IDataObject = {};
|
|
||||||
|
|
||||||
message = message.toString();
|
const parsePayload = (topic: string, payload: Buffer) => {
|
||||||
|
let message = payload.toString();
|
||||||
|
|
||||||
if (options.jsonParseBody) {
|
if (options.jsonParseBody) {
|
||||||
try {
|
try {
|
||||||
message = JSON.parse(message.toString());
|
message = JSON.parse(message);
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
result.message = message;
|
let result: IDataObject = { message, topic };
|
||||||
result.topic = topic;
|
|
||||||
|
|
||||||
if (options.onlyMessage) {
|
if (options.onlyMessage) {
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
result = [message as string];
|
result = [message];
|
||||||
}
|
}
|
||||||
|
|
||||||
let responsePromise: IDeferredPromise<IRun> | undefined;
|
return [this.helpers.returnJsonArray([result])];
|
||||||
if (!parallelProcessing) {
|
|
||||||
responsePromise = await this.helpers.createDeferredPromise();
|
|
||||||
}
|
|
||||||
this.emit([this.helpers.returnJsonArray([result])], undefined, responsePromise);
|
|
||||||
if (responsePromise) {
|
|
||||||
await responsePromise.promise();
|
|
||||||
}
|
|
||||||
resolve(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
client.on('error', (error) => {
|
|
||||||
reject(error);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const manualTriggerFunction = async () =>
|
||||||
|
await new Promise<void>(async (resolve) => {
|
||||||
|
client.once('message', (topic, payload) => {
|
||||||
|
this.emit(parsePayload(topic, payload));
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
await client.subscribeAsync(topicsQoS);
|
||||||
|
});
|
||||||
|
|
||||||
if (this.getMode() === 'trigger') {
|
if (this.getMode() === 'trigger') {
|
||||||
void manualTriggerFunction();
|
const donePromise = !options.parallelProcessing
|
||||||
|
? await this.helpers.createDeferredPromise<IRun>()
|
||||||
|
: undefined;
|
||||||
|
client.on('message', async (topic, payload) => {
|
||||||
|
this.emit(parsePayload(topic, payload), undefined, donePromise);
|
||||||
|
await donePromise?.promise();
|
||||||
|
});
|
||||||
|
await client.subscribeAsync(topicsQoS);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function closeFunction() {
|
async function closeFunction() {
|
||||||
client.end();
|
await client.endAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
38
packages/nodes-base/nodes/MQTT/test/GenericFunctions.test.ts
Normal file
38
packages/nodes-base/nodes/MQTT/test/GenericFunctions.test.ts
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import { MqttClient } from 'mqtt';
|
||||||
|
import { mock } from 'jest-mock-extended';
|
||||||
|
|
||||||
|
import { createClient, type MqttCredential } from '../GenericFunctions';
|
||||||
|
|
||||||
|
describe('createClient', () => {
|
||||||
|
const mockConnect = jest.spyOn(MqttClient.prototype, 'connect').mockImplementation(function (
|
||||||
|
this: MqttClient,
|
||||||
|
) {
|
||||||
|
setImmediate(() => this.emit('connect', mock()));
|
||||||
|
return this;
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => jest.clearAllMocks());
|
||||||
|
|
||||||
|
it('should create a client with minimal credentials', async () => {
|
||||||
|
const credentials = mock<MqttCredential>({
|
||||||
|
protocol: 'mqtt',
|
||||||
|
host: 'localhost',
|
||||||
|
port: 1883,
|
||||||
|
clean: true,
|
||||||
|
clientId: 'testClient',
|
||||||
|
ssl: false,
|
||||||
|
});
|
||||||
|
const client = await createClient(credentials);
|
||||||
|
|
||||||
|
expect(mockConnect).toBeCalledTimes(1);
|
||||||
|
expect(client).toBeDefined();
|
||||||
|
expect(client).toBeInstanceOf(MqttClient);
|
||||||
|
expect(client.options).toMatchObject({
|
||||||
|
protocol: 'mqtt',
|
||||||
|
host: 'localhost',
|
||||||
|
port: 1883,
|
||||||
|
clean: true,
|
||||||
|
clientId: 'testClient',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
56
packages/nodes-base/nodes/MQTT/test/Mqtt.node.test.ts
Normal file
56
packages/nodes-base/nodes/MQTT/test/Mqtt.node.test.ts
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
import type { MqttClient } from 'mqtt';
|
||||||
|
import { mock } from 'jest-mock-extended';
|
||||||
|
import type { ICredentialDataDecryptedObject, IExecuteFunctions } from 'n8n-workflow';
|
||||||
|
|
||||||
|
import { Mqtt } from '../Mqtt.node';
|
||||||
|
import { createClient } from '../GenericFunctions';
|
||||||
|
|
||||||
|
jest.mock('../GenericFunctions', () => {
|
||||||
|
const mockMqttClient = mock<MqttClient>();
|
||||||
|
return {
|
||||||
|
createClient: jest.fn().mockResolvedValue(mockMqttClient),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('MQTT Node', () => {
|
||||||
|
const credentials = mock<ICredentialDataDecryptedObject>();
|
||||||
|
const executeFunctions = mock<IExecuteFunctions>();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
|
||||||
|
executeFunctions.getCredentials.calledWith('mqtt').mockResolvedValue(credentials);
|
||||||
|
executeFunctions.getInputData.mockReturnValue([{ json: { testing: true } }]);
|
||||||
|
executeFunctions.getNodeParameter.calledWith('topic', 0).mockReturnValue('test/topic');
|
||||||
|
executeFunctions.getNodeParameter.calledWith('options', 0).mockReturnValue({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should publish input data', async () => {
|
||||||
|
executeFunctions.getNodeParameter.calledWith('sendInputData', 0).mockReturnValue(true);
|
||||||
|
|
||||||
|
const result = await new Mqtt().execute.call(executeFunctions);
|
||||||
|
|
||||||
|
expect(result).toEqual([[{ json: { testing: true } }]]);
|
||||||
|
expect(executeFunctions.getCredentials).toHaveBeenCalledTimes(1);
|
||||||
|
expect(executeFunctions.getNodeParameter).toHaveBeenCalledTimes(3);
|
||||||
|
|
||||||
|
const mockMqttClient = await createClient(mock());
|
||||||
|
expect(mockMqttClient.publishAsync).toHaveBeenCalledWith('test/topic', '{"testing":true}', {});
|
||||||
|
expect(mockMqttClient.endAsync).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should publish a custom message', async () => {
|
||||||
|
executeFunctions.getNodeParameter.calledWith('sendInputData', 0).mockReturnValue(false);
|
||||||
|
executeFunctions.getNodeParameter.calledWith('message', 0).mockReturnValue('Hello, MQTT!');
|
||||||
|
|
||||||
|
const result = await new Mqtt().execute.call(executeFunctions);
|
||||||
|
|
||||||
|
expect(result).toEqual([[{ json: { testing: true } }]]);
|
||||||
|
expect(executeFunctions.getCredentials).toHaveBeenCalledTimes(1);
|
||||||
|
expect(executeFunctions.getNodeParameter).toHaveBeenCalledTimes(4);
|
||||||
|
|
||||||
|
const mockMqttClient = await createClient(mock());
|
||||||
|
expect(mockMqttClient.publishAsync).toHaveBeenCalledWith('test/topic', 'Hello, MQTT!', {});
|
||||||
|
expect(mockMqttClient.endAsync).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
123
packages/nodes-base/nodes/MQTT/test/MqttTrigger.node.test.ts
Normal file
123
packages/nodes-base/nodes/MQTT/test/MqttTrigger.node.test.ts
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
import type { MqttClient, OnMessageCallback } from 'mqtt';
|
||||||
|
import { returnJsonArray } from 'n8n-core';
|
||||||
|
import { captor, mock } from 'jest-mock-extended';
|
||||||
|
import type { ICredentialDataDecryptedObject, ITriggerFunctions } from 'n8n-workflow';
|
||||||
|
|
||||||
|
import { MqttTrigger } from '../MqttTrigger.node';
|
||||||
|
import { createClient } from '../GenericFunctions';
|
||||||
|
|
||||||
|
jest.mock('../GenericFunctions', () => {
|
||||||
|
const mockMqttClient = mock<MqttClient>();
|
||||||
|
return {
|
||||||
|
createClient: jest.fn().mockResolvedValue(mockMqttClient),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('MQTT Trigger Node', () => {
|
||||||
|
const topic = 'test/topic';
|
||||||
|
const payload = Buffer.from('{"testing": true}');
|
||||||
|
const credentials = mock<ICredentialDataDecryptedObject>();
|
||||||
|
const triggerFunctions = mock<ITriggerFunctions>({
|
||||||
|
helpers: { returnJsonArray },
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
|
||||||
|
triggerFunctions.getCredentials.calledWith('mqtt').mockResolvedValue(credentials);
|
||||||
|
triggerFunctions.getNodeParameter.calledWith('topics').mockReturnValue(topic);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should emit in manual mode', async () => {
|
||||||
|
triggerFunctions.getMode.mockReturnValue('manual');
|
||||||
|
triggerFunctions.getNodeParameter.calledWith('options').mockReturnValue({});
|
||||||
|
|
||||||
|
const response = await new MqttTrigger().trigger.call(triggerFunctions);
|
||||||
|
expect(response.manualTriggerFunction).toBeDefined();
|
||||||
|
expect(response.closeFunction).toBeDefined();
|
||||||
|
|
||||||
|
expect(triggerFunctions.getCredentials).toHaveBeenCalledTimes(1);
|
||||||
|
expect(triggerFunctions.getNodeParameter).toHaveBeenCalledTimes(2);
|
||||||
|
|
||||||
|
// manually trigger the node, like Workflow.runNode does
|
||||||
|
const triggerPromise = response.manualTriggerFunction!();
|
||||||
|
|
||||||
|
const mockMqttClient = await createClient(mock());
|
||||||
|
expect(mockMqttClient.on).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
const onMessageCaptor = captor<OnMessageCallback>();
|
||||||
|
expect(mockMqttClient.once).toHaveBeenCalledWith('message', onMessageCaptor);
|
||||||
|
expect(mockMqttClient.subscribeAsync).toHaveBeenCalledWith({ [topic]: { qos: 0 } });
|
||||||
|
expect(triggerFunctions.emit).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
// simulate a message
|
||||||
|
const onMessage = onMessageCaptor.value;
|
||||||
|
onMessage('test/topic', payload, mock());
|
||||||
|
expect(triggerFunctions.emit).toHaveBeenCalledWith([
|
||||||
|
[{ json: { message: '{"testing": true}', topic } }],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// wait for the promise to resolve
|
||||||
|
await new Promise((resolve) => setImmediate(resolve));
|
||||||
|
await expect(triggerPromise).resolves.toEqual(undefined);
|
||||||
|
|
||||||
|
expect(mockMqttClient.endAsync).not.toHaveBeenCalled();
|
||||||
|
await response.closeFunction!();
|
||||||
|
expect(mockMqttClient.endAsync).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should emit in trigger mode', async () => {
|
||||||
|
triggerFunctions.getMode.mockReturnValue('trigger');
|
||||||
|
triggerFunctions.getNodeParameter.calledWith('options').mockReturnValue({});
|
||||||
|
|
||||||
|
const response = await new MqttTrigger().trigger.call(triggerFunctions);
|
||||||
|
expect(response.manualTriggerFunction).toBeDefined();
|
||||||
|
expect(response.closeFunction).toBeDefined();
|
||||||
|
|
||||||
|
expect(triggerFunctions.getCredentials).toHaveBeenCalledTimes(1);
|
||||||
|
expect(triggerFunctions.getNodeParameter).toHaveBeenCalledTimes(2);
|
||||||
|
|
||||||
|
const mockMqttClient = await createClient(mock());
|
||||||
|
expect(mockMqttClient.once).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
const onMessageCaptor = captor<OnMessageCallback>();
|
||||||
|
expect(mockMqttClient.on).toHaveBeenCalledWith('message', onMessageCaptor);
|
||||||
|
expect(mockMqttClient.subscribeAsync).toHaveBeenCalledWith({ [topic]: { qos: 0 } });
|
||||||
|
expect(triggerFunctions.emit).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
// simulate a message
|
||||||
|
const onMessage = onMessageCaptor.value;
|
||||||
|
onMessage('test/topic', payload, mock());
|
||||||
|
expect(triggerFunctions.emit).toHaveBeenCalledWith(
|
||||||
|
[[{ json: { message: '{"testing": true}', topic } }]],
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockMqttClient.endAsync).not.toHaveBeenCalled();
|
||||||
|
await response.closeFunction!();
|
||||||
|
expect(mockMqttClient.endAsync).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse JSON messages when configured', async () => {
|
||||||
|
triggerFunctions.getMode.mockReturnValue('trigger');
|
||||||
|
triggerFunctions.getNodeParameter.calledWith('options').mockReturnValue({
|
||||||
|
jsonParseBody: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
await new MqttTrigger().trigger.call(triggerFunctions);
|
||||||
|
|
||||||
|
const mockMqttClient = await createClient(mock());
|
||||||
|
const onMessageCaptor = captor<OnMessageCallback>();
|
||||||
|
expect(mockMqttClient.on).toHaveBeenCalledWith('message', onMessageCaptor);
|
||||||
|
|
||||||
|
// simulate a message
|
||||||
|
const onMessage = onMessageCaptor.value;
|
||||||
|
onMessage('test/topic', payload, mock());
|
||||||
|
expect(triggerFunctions.emit).toHaveBeenCalledWith(
|
||||||
|
[[{ json: { message: { testing: true }, topic } }]],
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
|
@ -873,7 +873,7 @@
|
||||||
"minifaker": "1.34.1",
|
"minifaker": "1.34.1",
|
||||||
"moment-timezone": "0.5.37",
|
"moment-timezone": "0.5.37",
|
||||||
"mongodb": "6.3.0",
|
"mongodb": "6.3.0",
|
||||||
"mqtt": "5.0.2",
|
"mqtt": "5.7.2",
|
||||||
"mssql": "10.0.2",
|
"mssql": "10.0.2",
|
||||||
"mysql2": "3.10.0",
|
"mysql2": "3.10.0",
|
||||||
"n8n-workflow": "workspace:*",
|
"n8n-workflow": "workspace:*",
|
||||||
|
|
105
pnpm-lock.yaml
105
pnpm-lock.yaml
|
@ -1456,8 +1456,8 @@ importers:
|
||||||
specifier: 6.3.0
|
specifier: 6.3.0
|
||||||
version: 6.3.0(gcp-metadata@5.2.0(encoding@0.1.13))(socks@2.7.1)
|
version: 6.3.0(gcp-metadata@5.2.0(encoding@0.1.13))(socks@2.7.1)
|
||||||
mqtt:
|
mqtt:
|
||||||
specifier: 5.0.2
|
specifier: 5.7.2
|
||||||
version: 5.0.2
|
version: 5.7.2
|
||||||
mssql:
|
mssql:
|
||||||
specifier: 10.0.2
|
specifier: 10.0.2
|
||||||
version: 10.0.2
|
version: 10.0.2
|
||||||
|
@ -2929,6 +2929,10 @@ packages:
|
||||||
resolution: {integrity: sha512-zHd0eUrf5GZoOWVCXp6koAKQTfZV07eit6bGPmJgnZdnSAvvZee6zniW2XMF7Cmc4ISOOnPy3QaSiIJGJkVEDQ==}
|
resolution: {integrity: sha512-zHd0eUrf5GZoOWVCXp6koAKQTfZV07eit6bGPmJgnZdnSAvvZee6zniW2XMF7Cmc4ISOOnPy3QaSiIJGJkVEDQ==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
|
'@babel/runtime@7.24.7':
|
||||||
|
resolution: {integrity: sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==}
|
||||||
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
'@babel/standalone@7.23.6':
|
'@babel/standalone@7.23.6':
|
||||||
resolution: {integrity: sha512-+AzS6BZwZdSosrgS/TiGDYLxtlefARKClWgJ4ql//XfmV9KbPWbkEekvbvDRJ8a6qog8E9j3CziHLz5dbIEMyw==}
|
resolution: {integrity: sha512-+AzS6BZwZdSosrgS/TiGDYLxtlefARKClWgJ4ql//XfmV9KbPWbkEekvbvDRJ8a6qog8E9j3CziHLz5dbIEMyw==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
@ -6601,9 +6605,6 @@ packages:
|
||||||
bl@4.1.0:
|
bl@4.1.0:
|
||||||
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
|
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
|
||||||
|
|
||||||
bl@5.0.0:
|
|
||||||
resolution: {integrity: sha512-8vxFNZ0pflFfi0WXA3WQXlj6CaMEwsmh63I1CNp0q+wWv8sD0ARx1KovSQd0l2GkwrMIOyedq0EF1FxI+RCZLQ==}
|
|
||||||
|
|
||||||
bl@6.0.12:
|
bl@6.0.12:
|
||||||
resolution: {integrity: sha512-EnEYHilP93oaOa2MnmNEjAcovPS3JlQZOyzGXi3EyEpPhm9qWvdDp7BmAVEVusGzp8LlwQK56Av+OkDoRjzE0w==}
|
resolution: {integrity: sha512-EnEYHilP93oaOa2MnmNEjAcovPS3JlQZOyzGXi3EyEpPhm9qWvdDp7BmAVEVusGzp8LlwQK56Av+OkDoRjzE0w==}
|
||||||
|
|
||||||
|
@ -8193,6 +8194,10 @@ packages:
|
||||||
fast-text-encoding@1.0.6:
|
fast-text-encoding@1.0.6:
|
||||||
resolution: {integrity: sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w==}
|
resolution: {integrity: sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w==}
|
||||||
|
|
||||||
|
fast-unique-numbers@8.0.13:
|
||||||
|
resolution: {integrity: sha512-7OnTFAVPefgw2eBJ1xj2PGGR9FwYzSUso9decayHgCDX4sJkHLdcsYTytTg+tYv+wKF3U8gJuSBz2jJpQV4u/g==}
|
||||||
|
engines: {node: '>=16.1.0'}
|
||||||
|
|
||||||
fast-xml-parser@4.2.5:
|
fast-xml-parser@4.2.5:
|
||||||
resolution: {integrity: sha512-B9/wizE4WngqQftFPmdaMYlXoJlJOYxGQOanC77fq9k8+Z0v5dDSVh+3glErdIROP//s/jgb7ZuxKfB8nVyo0g==}
|
resolution: {integrity: sha512-B9/wizE4WngqQftFPmdaMYlXoJlJOYxGQOanC77fq9k8+Z0v5dDSVh+3glErdIROP//s/jgb7ZuxKfB8nVyo0g==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
@ -8736,8 +8741,8 @@ packages:
|
||||||
resolution: {integrity: sha512-g+HZqgfbpXdCkme/Cd/mZkV0aV3BZZZSugecH03kl38m/Kmdx8jKjBikpDj2cr+Iynv4KpYEviojNdTJActJAg==}
|
resolution: {integrity: sha512-g+HZqgfbpXdCkme/Cd/mZkV0aV3BZZZSugecH03kl38m/Kmdx8jKjBikpDj2cr+Iynv4KpYEviojNdTJActJAg==}
|
||||||
engines: {node: '>=16.0.0'}
|
engines: {node: '>=16.0.0'}
|
||||||
|
|
||||||
help-me@4.2.0:
|
help-me@5.0.0:
|
||||||
resolution: {integrity: sha512-TAOnTB8Tz5Dw8penUuzHVrKNKlCIbwwbHnXraNJxPwf8LRtE2HlM84RYuezMFcwOJmoYOCWVDyJ8TQGxn9PgxA==}
|
resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==}
|
||||||
|
|
||||||
hexoid@1.0.0:
|
hexoid@1.0.0:
|
||||||
resolution: {integrity: sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==}
|
resolution: {integrity: sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==}
|
||||||
|
@ -9994,6 +9999,10 @@ packages:
|
||||||
lower-case@2.0.2:
|
lower-case@2.0.2:
|
||||||
resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==}
|
resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==}
|
||||||
|
|
||||||
|
lru-cache@10.2.2:
|
||||||
|
resolution: {integrity: sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==}
|
||||||
|
engines: {node: 14 || >=16.14}
|
||||||
|
|
||||||
lru-cache@4.1.5:
|
lru-cache@4.1.5:
|
||||||
resolution: {integrity: sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==}
|
resolution: {integrity: sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==}
|
||||||
|
|
||||||
|
@ -10393,11 +10402,11 @@ packages:
|
||||||
socks:
|
socks:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
mqtt-packet@8.2.0:
|
mqtt-packet@9.0.0:
|
||||||
resolution: {integrity: sha512-21Vo7XdRXUw2qhdTfk8GeOl2jtb8Dkwd4dKxn/epvf37mxTxHodvBJoozTPZGVwh57JXlsh2ChsaxMsAfqxp+A==}
|
resolution: {integrity: sha512-8v+HkX+fwbodsWAZIZTI074XIoxVBOmPeggQuDFCGg1SqNcC+uoRMWu7J6QlJPqIUIJXmjNYYHxBBLr1Y/Df4w==}
|
||||||
|
|
||||||
mqtt@5.0.2:
|
mqtt@5.7.2:
|
||||||
resolution: {integrity: sha512-JctWQpxjVVjn5LqAAhfgutMNMbHZf+puG9LnbWga0wRbWW7QVfHPu4Vz2iDSyHfGpW8HWLF+jb01vySDDzIKnQ==}
|
resolution: {integrity: sha512-b5xIA9J/K1LTubSWKaNYYLxYIusQdip6o9/8bRWad2TelRr8xLifjQt+SnamDAwMp3O6NdvR9E8ae7VMuN02kg==}
|
||||||
engines: {node: '>=16.0.0'}
|
engines: {node: '>=16.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
@ -13346,8 +13355,8 @@ packages:
|
||||||
vue-component-type-helpers@2.0.19:
|
vue-component-type-helpers@2.0.19:
|
||||||
resolution: {integrity: sha512-cN3f1aTxxKo4lzNeQAkVopswuImUrb5Iurll9Gaw5cqpnbTAxtEMM1mgi6ou4X79OCyqYv1U1mzBHJkzmiK82w==}
|
resolution: {integrity: sha512-cN3f1aTxxKo4lzNeQAkVopswuImUrb5Iurll9Gaw5cqpnbTAxtEMM1mgi6ou4X79OCyqYv1U1mzBHJkzmiK82w==}
|
||||||
|
|
||||||
vue-component-type-helpers@2.0.21:
|
vue-component-type-helpers@2.0.22:
|
||||||
resolution: {integrity: sha512-3NaicyZ7N4B6cft4bfb7dOnPbE9CjLcx+6wZWAg5zwszfO4qXRh+U52dN5r5ZZfc6iMaxKCEcoH9CmxxoFZHLg==}
|
resolution: {integrity: sha512-gPr2Ba7efUwy/Vfbuf735bHSVdN4ycoZUCHfypkI33M9DUH+ieRblLLVM2eImccFYaWNWwEzURx02EgoXDBmaQ==}
|
||||||
|
|
||||||
vue-demi@0.14.5:
|
vue-demi@0.14.5:
|
||||||
resolution: {integrity: sha512-o9NUVpl/YlsGJ7t+xuqJKx8EBGf1quRhCiT6D/J0pfwmk9zUwYkC7yrF4SZCe6fETvSM3UNL2edcbYrSyc4QHA==}
|
resolution: {integrity: sha512-o9NUVpl/YlsGJ7t+xuqJKx8EBGf1quRhCiT6D/J0pfwmk9zUwYkC7yrF4SZCe6fETvSM3UNL2edcbYrSyc4QHA==}
|
||||||
|
@ -13584,6 +13593,15 @@ packages:
|
||||||
wordwrap@1.0.0:
|
wordwrap@1.0.0:
|
||||||
resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==}
|
resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==}
|
||||||
|
|
||||||
|
worker-timers-broker@6.1.8:
|
||||||
|
resolution: {integrity: sha512-FUCJu9jlK3A8WqLTKXM9E6kAmI/dR1vAJ8dHYLMisLNB/n3GuaFIjJ7pn16ZcD1zCOf7P6H62lWIEBi+yz/zQQ==}
|
||||||
|
|
||||||
|
worker-timers-worker@7.0.71:
|
||||||
|
resolution: {integrity: sha512-ks/5YKwZsto1c2vmljroppOKCivB/ma97g9y77MAAz2TBBjPPgpoOiS1qYQKIgvGTr2QYPT3XhJWIB6Rj2MVPQ==}
|
||||||
|
|
||||||
|
worker-timers@7.1.8:
|
||||||
|
resolution: {integrity: sha512-R54psRKYVLuzff7c1OTFcq/4Hue5Vlz4bFtNEIarpSiCYhpifHU3aIQI29S84o1j87ePCYqbmEJPqwBTf+3sfw==}
|
||||||
|
|
||||||
wrap-ansi@6.2.0:
|
wrap-ansi@6.2.0:
|
||||||
resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
|
resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
@ -15889,6 +15907,10 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
regenerator-runtime: 0.14.1
|
regenerator-runtime: 0.14.1
|
||||||
|
|
||||||
|
'@babel/runtime@7.24.7':
|
||||||
|
dependencies:
|
||||||
|
regenerator-runtime: 0.14.1
|
||||||
|
|
||||||
'@babel/standalone@7.23.6': {}
|
'@babel/standalone@7.23.6': {}
|
||||||
|
|
||||||
'@babel/template@7.22.5':
|
'@babel/template@7.22.5':
|
||||||
|
@ -18976,7 +18998,7 @@ snapshots:
|
||||||
ts-dedent: 2.2.0
|
ts-dedent: 2.2.0
|
||||||
type-fest: 2.19.0
|
type-fest: 2.19.0
|
||||||
vue: 3.4.21(typescript@5.5.2)
|
vue: 3.4.21(typescript@5.5.2)
|
||||||
vue-component-type-helpers: 2.0.21
|
vue-component-type-helpers: 2.0.22
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- encoding
|
- encoding
|
||||||
- prettier
|
- prettier
|
||||||
|
@ -20623,12 +20645,6 @@ snapshots:
|
||||||
inherits: 2.0.4
|
inherits: 2.0.4
|
||||||
readable-stream: 3.6.0
|
readable-stream: 3.6.0
|
||||||
|
|
||||||
bl@5.0.0:
|
|
||||||
dependencies:
|
|
||||||
buffer: 6.0.3
|
|
||||||
inherits: 2.0.4
|
|
||||||
readable-stream: 3.6.0
|
|
||||||
|
|
||||||
bl@6.0.12:
|
bl@6.0.12:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/readable-stream': 4.0.10
|
'@types/readable-stream': 4.0.10
|
||||||
|
@ -22630,6 +22646,11 @@ snapshots:
|
||||||
|
|
||||||
fast-text-encoding@1.0.6: {}
|
fast-text-encoding@1.0.6: {}
|
||||||
|
|
||||||
|
fast-unique-numbers@8.0.13:
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.24.7
|
||||||
|
tslib: 2.6.2
|
||||||
|
|
||||||
fast-xml-parser@4.2.5:
|
fast-xml-parser@4.2.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
strnum: 1.0.5
|
strnum: 1.0.5
|
||||||
|
@ -23294,10 +23315,7 @@ snapshots:
|
||||||
|
|
||||||
helmet@7.1.0: {}
|
helmet@7.1.0: {}
|
||||||
|
|
||||||
help-me@4.2.0:
|
help-me@5.0.0: {}
|
||||||
dependencies:
|
|
||||||
glob: 8.1.0
|
|
||||||
readable-stream: 3.6.0
|
|
||||||
|
|
||||||
hexoid@1.0.0: {}
|
hexoid@1.0.0: {}
|
||||||
|
|
||||||
|
@ -24769,6 +24787,8 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.6.2
|
tslib: 2.6.2
|
||||||
|
|
||||||
|
lru-cache@10.2.2: {}
|
||||||
|
|
||||||
lru-cache@4.1.5:
|
lru-cache@4.1.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
pseudomap: 1.0.2
|
pseudomap: 1.0.2
|
||||||
|
@ -25159,29 +25179,31 @@ snapshots:
|
||||||
gcp-metadata: 5.2.0(encoding@0.1.13)
|
gcp-metadata: 5.2.0(encoding@0.1.13)
|
||||||
socks: 2.7.1
|
socks: 2.7.1
|
||||||
|
|
||||||
mqtt-packet@8.2.0:
|
mqtt-packet@9.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
bl: 5.0.0
|
bl: 6.0.12
|
||||||
debug: 4.3.4(supports-color@8.1.1)
|
debug: 4.3.4(supports-color@8.1.1)
|
||||||
process-nextick-args: 2.0.1
|
process-nextick-args: 2.0.1
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
mqtt@5.0.2:
|
mqtt@5.7.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@types/readable-stream': 4.0.10
|
||||||
|
'@types/ws': 8.5.10
|
||||||
commist: 3.2.0
|
commist: 3.2.0
|
||||||
concat-stream: 2.0.0
|
concat-stream: 2.0.0
|
||||||
debug: 4.3.4(supports-color@8.1.1)
|
debug: 4.3.4(supports-color@8.1.1)
|
||||||
duplexify: 4.1.2
|
help-me: 5.0.0
|
||||||
help-me: 4.2.0
|
lru-cache: 10.2.2
|
||||||
lru-cache: 7.18.3
|
|
||||||
minimist: 1.2.8
|
minimist: 1.2.8
|
||||||
mqtt-packet: 8.2.0
|
mqtt-packet: 9.0.0
|
||||||
number-allocator: 1.0.14
|
number-allocator: 1.0.14
|
||||||
readable-stream: 4.4.2
|
readable-stream: 4.4.2
|
||||||
reinterval: 1.1.0
|
reinterval: 1.1.0
|
||||||
rfdc: 1.3.0
|
rfdc: 1.3.0
|
||||||
split2: 4.2.0
|
split2: 4.2.0
|
||||||
|
worker-timers: 7.1.8
|
||||||
ws: 8.17.1
|
ws: 8.17.1
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- bufferutil
|
- bufferutil
|
||||||
|
@ -28509,7 +28531,7 @@ snapshots:
|
||||||
|
|
||||||
vue-component-type-helpers@2.0.19: {}
|
vue-component-type-helpers@2.0.19: {}
|
||||||
|
|
||||||
vue-component-type-helpers@2.0.21: {}
|
vue-component-type-helpers@2.0.22: {}
|
||||||
|
|
||||||
vue-demi@0.14.5(vue@3.4.21(typescript@5.5.2)):
|
vue-demi@0.14.5(vue@3.4.21(typescript@5.5.2)):
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -28806,6 +28828,25 @@ snapshots:
|
||||||
|
|
||||||
wordwrap@1.0.0: {}
|
wordwrap@1.0.0: {}
|
||||||
|
|
||||||
|
worker-timers-broker@6.1.8:
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.24.7
|
||||||
|
fast-unique-numbers: 8.0.13
|
||||||
|
tslib: 2.6.2
|
||||||
|
worker-timers-worker: 7.0.71
|
||||||
|
|
||||||
|
worker-timers-worker@7.0.71:
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.24.7
|
||||||
|
tslib: 2.6.2
|
||||||
|
|
||||||
|
worker-timers@7.1.8:
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.24.7
|
||||||
|
tslib: 2.6.2
|
||||||
|
worker-timers-broker: 6.1.8
|
||||||
|
worker-timers-worker: 7.0.71
|
||||||
|
|
||||||
wrap-ansi@6.2.0:
|
wrap-ansi@6.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
ansi-styles: 4.3.0
|
ansi-styles: 4.3.0
|
||||||
|
|
Loading…
Reference in a new issue