2024-11-27 19:28:24 +02:00
|
|
|
///
|
2025-02-25 09:39:16 +02:00
|
|
|
/// Copyright © 2016-2025 The Thingsboard Authors
|
2024-11-27 19:28:24 +02:00
|
|
|
///
|
|
|
|
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
|
/// you may not use this file except in compliance with the License.
|
|
|
|
|
/// You may obtain a copy of the License at
|
|
|
|
|
///
|
|
|
|
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
///
|
|
|
|
|
/// Unless required by applicable law or agreed to in writing, software
|
|
|
|
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
|
/// See the License for the specific language governing permissions and
|
|
|
|
|
/// limitations under the License.
|
|
|
|
|
///
|
|
|
|
|
|
2024-11-28 20:05:26 +02:00
|
|
|
import { forkJoin, from, map, mergeMap, Observable, of, ReplaySubject, switchMap } from 'rxjs';
|
|
|
|
|
import { removeTbResourcePrefix, ResourceInfo } from '@shared/models/resource.models';
|
2024-11-27 19:28:24 +02:00
|
|
|
import { HttpClient } from '@angular/common/http';
|
|
|
|
|
import { defaultHttpOptionsFromConfig } from '@core/http/http-utils';
|
2024-11-28 20:05:26 +02:00
|
|
|
import { TbEditorCompleter, TbEditorCompletion } from '@shared/models/ace/completion.models';
|
|
|
|
|
import { blobToText } from '@core/utils';
|
|
|
|
|
import { catchError, finalize } from 'rxjs/operators';
|
|
|
|
|
import { parseError } from '@shared/models/error.models';
|
2024-11-29 13:49:34 +02:00
|
|
|
import { TranslateService } from '@ngx-translate/core';
|
2024-11-27 19:28:24 +02:00
|
|
|
|
|
|
|
|
export interface TbFunctionWithModules {
|
|
|
|
|
body: string;
|
|
|
|
|
modules: {[alias: string]: string };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export type TbFunction = string | TbFunctionWithModules;
|
|
|
|
|
|
|
|
|
|
export const isNotEmptyTbFunction = (tbFunction: TbFunction): boolean => {
|
|
|
|
|
if (tbFunction) {
|
|
|
|
|
if (typeof tbFunction === 'string') {
|
|
|
|
|
return tbFunction.trim().length > 0;
|
|
|
|
|
} else {
|
|
|
|
|
return tbFunction.body && tbFunction.body.trim().length > 0;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
2024-11-29 17:40:47 +02:00
|
|
|
return false;
|
2024-11-27 19:28:24 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-29 17:40:47 +02:00
|
|
|
export const compileTbFunction = <T extends GenericFunction>(http: HttpClient, tbFunction: TbFunction, ...args: string[]): Observable<CompiledTbFunction<T>> => {
|
2024-11-27 19:28:24 +02:00
|
|
|
let functionBody: string;
|
|
|
|
|
let functionArgs: string[];
|
|
|
|
|
let modules: {[alias: string]: string };
|
|
|
|
|
if (typeof tbFunction === 'string') {
|
|
|
|
|
functionBody = tbFunction;
|
|
|
|
|
functionArgs = args;
|
|
|
|
|
} else {
|
|
|
|
|
functionBody = tbFunction.body;
|
|
|
|
|
modules = tbFunction.modules;
|
|
|
|
|
const modulesArgs = Object.keys(tbFunction.modules);
|
|
|
|
|
functionArgs = args.concat(modulesArgs);
|
|
|
|
|
}
|
|
|
|
|
return loadFunctionModules(http, modules).pipe(
|
|
|
|
|
map((compiledModules) => {
|
|
|
|
|
const compiledFunction = new Function(...functionArgs, functionBody);
|
2024-11-29 17:40:47 +02:00
|
|
|
return new CompiledTbFunction<T>(compiledFunction, compiledModules);
|
2024-11-27 19:28:24 +02:00
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-28 20:05:26 +02:00
|
|
|
export const loadModulesCompleter = (http: HttpClient, modules: {[alias: string]: string }): Observable<TbEditorCompleter | null> => {
|
|
|
|
|
if (!modules || !Object.keys(modules).length) {
|
|
|
|
|
return of(null);
|
|
|
|
|
} else {
|
|
|
|
|
const modulesDescription: {[alias: string]: Observable<TbEditorCompletion>} = {};
|
|
|
|
|
for (const alias of Object.keys(modules)) {
|
|
|
|
|
modulesDescription[alias] = loadModuleCompletion(http, modules[alias]);
|
|
|
|
|
}
|
|
|
|
|
return forkJoin(modulesDescription).pipe(
|
|
|
|
|
map((completions) => {
|
|
|
|
|
return new TbEditorCompleter(completions);
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2024-11-29 13:49:34 +02:00
|
|
|
export const loadModuleMarkdownDescription = (http: HttpClient, translate: TranslateService, resource: ResourceInfo): Observable<string> => {
|
|
|
|
|
let description = `<div class="flex flex-col !pl-4 !pr-4"><h6>${resource.title}</h6><small>${translate.instant('js-func.module-members')}</small></div>\n\n`;
|
2024-11-28 20:05:26 +02:00
|
|
|
description += '<div class="divider !pt-2"></div>\n' +
|
|
|
|
|
'<br/>\n\n';
|
|
|
|
|
return loadFunctionModuleWithSource(http, resource.link).pipe(
|
|
|
|
|
map((moduleWithSource) => {
|
|
|
|
|
const module = moduleWithSource.module;
|
|
|
|
|
const propertiesData: { type: 'function' | 'const', propName: string, description: string }[] = [];
|
|
|
|
|
for (const propName of Object.keys(module)) {
|
|
|
|
|
let propDescription = '';
|
|
|
|
|
const prop = module[propName];
|
|
|
|
|
const type = typeof prop;
|
|
|
|
|
if (type === 'function') {
|
|
|
|
|
const funcArgs = getFunctionArguments(prop);
|
|
|
|
|
propDescription += `<p class="!pl-4 !pr-4"><em>function</em> <strong>${propName}</strong> <em>(${funcArgs.join(', ')})</em>: <code>any</code></p>`;
|
|
|
|
|
} else {
|
|
|
|
|
propDescription += `<p class="!pl-4 !pr-4"><em>const</em> <strong>${propName}</strong>: <code>${type}</code>`;
|
|
|
|
|
if (type !== 'object') {
|
2024-12-26 12:28:31 +02:00
|
|
|
propDescription += ' = ' + (type === 'string' ? `"${handleHtmlSpecialChars(prop)}"` : `${prop}`);
|
2024-11-28 20:05:26 +02:00
|
|
|
}
|
|
|
|
|
propDescription += '</p>';
|
|
|
|
|
}
|
|
|
|
|
propDescription += '\n\n';
|
|
|
|
|
const propertyData: { type: 'function' | 'const', propName: string, description: string } = {
|
|
|
|
|
type: type === 'function' ? 'function' : 'const',
|
|
|
|
|
propName,
|
|
|
|
|
description: propDescription
|
|
|
|
|
}
|
|
|
|
|
propertiesData.push(propertyData);
|
|
|
|
|
}
|
|
|
|
|
propertiesData.sort((a, b) => {
|
|
|
|
|
if (a.type === b.type) {
|
|
|
|
|
return a.propName.localeCompare(b.propName);
|
|
|
|
|
} else if (a.type === 'const') return -1;
|
|
|
|
|
else return 1;
|
|
|
|
|
});
|
|
|
|
|
if (!propertiesData.length) {
|
2024-11-29 13:49:34 +02:00
|
|
|
description += `<div class="!pl-4 !pr-4">${translate.instant('js-func.module-no-members')}</div>\n\n`;
|
2024-11-28 20:05:26 +02:00
|
|
|
} else {
|
|
|
|
|
propertiesData.forEach((pData) => {
|
|
|
|
|
description += pData.description;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
return description;
|
|
|
|
|
}),
|
|
|
|
|
catchError(err => {
|
|
|
|
|
const errorText = parseError(err);
|
2024-11-29 13:49:34 +02:00
|
|
|
description += `<div class="!pl-4 !pr-4">${translate.instant('js-func.module-load-error')}:<br/><span style="color: red;">${errorText}</span></div>\n\n`;
|
2024-11-28 20:05:26 +02:00
|
|
|
return of(description);
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-24 17:12:45 +02:00
|
|
|
const handleHtmlSpecialChars = (text: string): string => {
|
|
|
|
|
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-29 13:49:34 +02:00
|
|
|
export const loadModuleMarkdownSourceCode = (http: HttpClient, translate: TranslateService, resource: ResourceInfo): Observable<string> => {
|
|
|
|
|
let sourceCode = `<div class="flex flex-col !pl-4"><h6>${resource.title}</h6><small>${translate.instant('js-func.source-code')}</small></div>\n\n`;
|
2024-11-28 20:05:26 +02:00
|
|
|
return loadFunctionModuleSource(http, resource.link).pipe(
|
|
|
|
|
map((source) => {
|
|
|
|
|
sourceCode += '```javascript\n{:code-style="margin-left: -16px; margin-right: -16px;"}\n' + source + '\n```';
|
|
|
|
|
return sourceCode;
|
|
|
|
|
}),
|
|
|
|
|
catchError(err => {
|
|
|
|
|
const errorText = parseError(err);
|
2024-11-29 13:49:34 +02:00
|
|
|
sourceCode += `<div class="!pl-4 !pr-4">${translate.instant('js-func.source-code-load-error')}:<br/><span style="color: red;">${errorText}</span></div>\n\n`;
|
2024-11-28 20:05:26 +02:00
|
|
|
return of(sourceCode);
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const loadModuleCompletion = (http: HttpClient, moduleLink: string): Observable<TbEditorCompletion> => {
|
|
|
|
|
return loadFunctionModule(http, moduleLink).pipe(
|
|
|
|
|
map((module) => {
|
|
|
|
|
const completion: TbEditorCompletion = {
|
|
|
|
|
meta: 'module',
|
|
|
|
|
type: 'module',
|
|
|
|
|
children: {}
|
|
|
|
|
};
|
|
|
|
|
for (const propName of Object.keys(module)) {
|
|
|
|
|
const prop = module[propName];
|
|
|
|
|
const type = typeof prop;
|
|
|
|
|
const propertyCompletion: TbEditorCompletion = {
|
|
|
|
|
meta: type === 'function' ? 'function' : 'constant',
|
|
|
|
|
type
|
|
|
|
|
};
|
|
|
|
|
if (type === 'function') {
|
|
|
|
|
propertyCompletion.args = getFunctionArguments(prop).map(functionArg => {
|
|
|
|
|
return {name: functionArg}
|
|
|
|
|
});
|
|
|
|
|
propertyCompletion.return = { type: 'any'};
|
|
|
|
|
} else if (type !== 'object') {
|
|
|
|
|
propertyCompletion.description = `<div class="tb-api-title">Constant value:</div><code class="title">${prop}</code>`;
|
|
|
|
|
}
|
|
|
|
|
completion.children[propName] = propertyCompletion;
|
|
|
|
|
}
|
|
|
|
|
return completion;
|
|
|
|
|
}),
|
|
|
|
|
catchError(err => {
|
|
|
|
|
const completion: TbEditorCompletion = {
|
|
|
|
|
meta: 'module',
|
|
|
|
|
type: 'module',
|
|
|
|
|
children: {}
|
|
|
|
|
};
|
|
|
|
|
const errorText = parseError(err);
|
|
|
|
|
completion.description = `<div>Module load error:<br/><span style="color: red;">${errorText}</span></div>`;
|
|
|
|
|
return of(completion);
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-02 16:35:52 +02:00
|
|
|
export type GenericFunction = (...args: any[]) => any;
|
2024-11-29 17:40:47 +02:00
|
|
|
|
|
|
|
|
export class CompiledTbFunction<T extends GenericFunction> {
|
|
|
|
|
|
|
|
|
|
public execute: T = this.executeImpl.bind(this);
|
2024-11-27 19:28:24 +02:00
|
|
|
|
|
|
|
|
constructor(private compiledFunction: Function,
|
|
|
|
|
private compiledModules: System.Module[]) {
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-29 17:40:47 +02:00
|
|
|
private executeImpl(...args: any[]): any {
|
2024-11-27 19:28:24 +02:00
|
|
|
let functionArgs: any[];
|
|
|
|
|
if (this.compiledModules?.length) {
|
2024-11-29 13:49:34 +02:00
|
|
|
functionArgs = args ? args.concat(this.compiledModules) : this.compiledModules;
|
2024-11-27 19:28:24 +02:00
|
|
|
} else {
|
|
|
|
|
functionArgs = args;
|
|
|
|
|
}
|
|
|
|
|
return this.compiledFunction(...functionArgs);
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-29 13:49:34 +02:00
|
|
|
apply(thisArg: any, argArray?: any): any {
|
|
|
|
|
let functionArgs: any[];
|
|
|
|
|
if (this.compiledModules?.length) {
|
|
|
|
|
functionArgs = argArray ? argArray.concat(this.compiledModules) : this.compiledModules;
|
|
|
|
|
} else {
|
|
|
|
|
functionArgs = argArray;
|
|
|
|
|
}
|
|
|
|
|
return this.compiledFunction.apply(thisArg, functionArgs);
|
|
|
|
|
}
|
2024-11-27 19:28:24 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const loadFunctionModules = (http: HttpClient, modules: {[alias: string]: string }): Observable<System.Module[]> => {
|
|
|
|
|
if (modules && Object.keys(modules).length) {
|
|
|
|
|
const moduleObservables: Observable<System.Module>[] = [];
|
|
|
|
|
for (const alias of Object.keys(modules)) {
|
|
|
|
|
moduleObservables.push(loadFunctionModule(http, modules[alias]));
|
|
|
|
|
}
|
|
|
|
|
return forkJoin(moduleObservables);
|
|
|
|
|
} else {
|
|
|
|
|
return of([]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const modulesLoading: {[url: string]: ReplaySubject<System.Module>} = {};
|
|
|
|
|
|
|
|
|
|
const loadFunctionModule = (http: HttpClient, moduleLink: string): Observable<System.Module> => {
|
|
|
|
|
const url = removeTbResourcePrefix(moduleLink);
|
|
|
|
|
let request: ReplaySubject<System.Module>;
|
|
|
|
|
if (modulesLoading[url]) {
|
|
|
|
|
request = modulesLoading[url];
|
|
|
|
|
} else {
|
|
|
|
|
request = new ReplaySubject<System.Module>(1);
|
|
|
|
|
modulesLoading[url] = request;
|
|
|
|
|
const options = defaultHttpOptionsFromConfig({ignoreLoading: true, ignoreErrors: true});
|
|
|
|
|
http.get(url, {...options, ...{ observe: 'response', responseType: 'blob' } }).pipe(
|
2024-11-28 20:05:26 +02:00
|
|
|
mergeMap((response) => {
|
2024-11-27 19:28:24 +02:00
|
|
|
const objectURL = URL.createObjectURL(response.body);
|
|
|
|
|
const asyncModule = from(import(/* @vite-ignore */objectURL));
|
|
|
|
|
URL.revokeObjectURL(objectURL);
|
|
|
|
|
return asyncModule;
|
2024-11-28 20:05:26 +02:00
|
|
|
}),
|
|
|
|
|
finalize(() => {
|
|
|
|
|
delete modulesLoading[url];
|
2024-11-27 19:28:24 +02:00
|
|
|
})
|
|
|
|
|
).subscribe(
|
|
|
|
|
{
|
|
|
|
|
next: (value) => {
|
|
|
|
|
request.next(value);
|
|
|
|
|
request.complete();
|
|
|
|
|
},
|
|
|
|
|
error: err => {
|
|
|
|
|
request.error(err);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
return request;
|
|
|
|
|
}
|
2024-11-28 20:05:26 +02:00
|
|
|
|
|
|
|
|
interface TbModuleWithSource {
|
|
|
|
|
module: System.Module;
|
|
|
|
|
source: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const loadFunctionModuleWithSource = (http: HttpClient, moduleLink: string): Observable<TbModuleWithSource> => {
|
|
|
|
|
const url = removeTbResourcePrefix(moduleLink);
|
|
|
|
|
const options = defaultHttpOptionsFromConfig({ignoreLoading: true, ignoreErrors: true});
|
|
|
|
|
return http.get(url, {...options, ...{ observe: 'response', responseType: 'blob' } }).pipe(
|
|
|
|
|
switchMap((response) => {
|
|
|
|
|
const objectURL = URL.createObjectURL(response.body);
|
|
|
|
|
const asyncModule = from(import(/* @vite-ignore */objectURL));
|
|
|
|
|
URL.revokeObjectURL(objectURL);
|
|
|
|
|
const asyncSource = blobToText(response.body);
|
|
|
|
|
return forkJoin({
|
|
|
|
|
module: asyncModule,
|
|
|
|
|
source: asyncSource
|
|
|
|
|
});
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const loadFunctionModuleSource = (http: HttpClient, moduleLink: string): Observable<string> => {
|
|
|
|
|
const url = removeTbResourcePrefix(moduleLink);
|
|
|
|
|
const options = defaultHttpOptionsFromConfig({ignoreLoading: true, ignoreErrors: true});
|
|
|
|
|
return http.get(url, {...options, ...{ responseType: 'text' } });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const getFunctionArguments = (func: Function): string[] => {
|
|
|
|
|
const fnStr = func.toString().replace(/((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg, '');
|
2024-11-29 17:40:47 +02:00
|
|
|
const firstBracketIndex = fnStr.indexOf('(');
|
|
|
|
|
const secondBracketIndex = fnStr.indexOf(')');
|
|
|
|
|
if (firstBracketIndex === -1 || secondBracketIndex === -1 || (secondBracketIndex - firstBracketIndex) <= 1) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
const match = fnStr.slice(firstBracketIndex+1, secondBracketIndex).match(/([^\s,]+)/g);
|
|
|
|
|
if (match) {
|
|
|
|
|
return new Array<string>(...match);
|
|
|
|
|
} else {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
2024-11-28 20:05:26 +02:00
|
|
|
}
|