mirror of
https://github.com/ToolJet/ToolJet
synced 2026-04-27 00:17:18 +00:00
230 lines
7.5 KiB
TypeScript
230 lines
7.5 KiB
TypeScript
import { QueryResult, QueryService, ConnectionTestResult, QueryError, getAuthUrl, getRefreshedToken } from '@tooljet-plugins/common';
|
|
import { SourceOptions, QueryOptions, GrpcService, GrpcOperationError, GrpcClient, toError } from './types';
|
|
import * as grpc from '@grpc/grpc-js';
|
|
import JSON5 from 'json5';
|
|
import {
|
|
buildReflectionClient,
|
|
buildProtoFileClient,
|
|
discoverServicesUsingReflection,
|
|
discoverServicesUsingProtoFile,
|
|
loadProtoFromRemoteUrl,
|
|
extractServicesFromGrpcPackage,
|
|
executeGrpcMethod
|
|
} from './operations';
|
|
import { PackageDefinition } from '@grpc/proto-loader';
|
|
|
|
export default class Grpcv2QueryService implements QueryService {
|
|
|
|
async run(
|
|
sourceOptions: SourceOptions,
|
|
queryOptions: QueryOptions,
|
|
_dataSourceId: string,
|
|
_dataSourceUpdatedAt: string
|
|
): Promise<QueryResult> {
|
|
try {
|
|
const client = await this.createGrpcClient(sourceOptions, queryOptions.service);
|
|
|
|
this.validateRequestData(queryOptions);
|
|
this.validateMethodExists(client, queryOptions);
|
|
|
|
const response = await this.executeGrpcCall(client, queryOptions, sourceOptions);
|
|
|
|
return {
|
|
status: 'ok',
|
|
data: response,
|
|
};
|
|
} catch (error: unknown) {
|
|
if (error instanceof GrpcOperationError) {
|
|
throw new QueryError('Query could not be completed', error.message, error.errorDetails);
|
|
}
|
|
|
|
const err = toError(error);
|
|
throw new QueryError('Query could not be completed', err.message || 'An unknown error occurred', {
|
|
grpcCode: 0,
|
|
grpcStatus: 'UNKNOWN',
|
|
errorType: 'QueryError'
|
|
});
|
|
}
|
|
}
|
|
|
|
async testConnection(sourceOptions: SourceOptions): Promise<ConnectionTestResult> {
|
|
try {
|
|
if (sourceOptions.proto_files === 'server_reflection') {
|
|
const services = await this.discoverServices(sourceOptions);
|
|
return {
|
|
status: 'ok',
|
|
message: `Successfully connected. Found ${services.length} service(s).`
|
|
};
|
|
} else {
|
|
let packageDefinition: PackageDefinition;
|
|
try {
|
|
packageDefinition = await loadProtoFromRemoteUrl(sourceOptions.proto_file_url!);
|
|
} catch (protoError) {
|
|
return {
|
|
status: 'failed',
|
|
message: `Proto file error: ${protoError?.message || 'Failed to load proto file from URL'}`,
|
|
};
|
|
}
|
|
|
|
const grpcObject = grpc.loadPackageDefinition(packageDefinition);
|
|
let services: GrpcService[];
|
|
|
|
try {
|
|
services = extractServicesFromGrpcPackage(grpcObject);
|
|
} catch (extractError) {
|
|
return {
|
|
status: 'failed',
|
|
message: 'No services found in proto file',
|
|
};
|
|
}
|
|
|
|
if (services.length === 0) {
|
|
return {
|
|
status: 'failed',
|
|
message: 'No services found in proto file',
|
|
};
|
|
}
|
|
|
|
const firstService = services[0].name;
|
|
try {
|
|
const client = await this.createGrpcClient(sourceOptions, firstService);
|
|
|
|
const deadline = new Date();
|
|
deadline.setSeconds(deadline.getSeconds() + 60);
|
|
|
|
const waitForReadyAsync = (client: GrpcClient, deadline: Date): Promise<void> => {
|
|
return new Promise((resolve, reject) => {
|
|
client.waitForReady(deadline, (error: any) => {
|
|
if (error) {
|
|
reject(error);
|
|
} else {
|
|
resolve();
|
|
}
|
|
});
|
|
});
|
|
};
|
|
|
|
try {
|
|
await waitForReadyAsync(client, deadline);
|
|
} catch (error) {
|
|
throw new Error(`Cannot connect to host: ${error.message}`);
|
|
}
|
|
|
|
return {
|
|
status: 'ok',
|
|
message: `Successfully connected. Proto file loaded with ${services.length} service(s).`
|
|
};
|
|
} catch (connectionError) {
|
|
return {
|
|
status: 'failed',
|
|
message: `Connection error: ${connectionError?.message || 'Failed to connect to gRPC server'}`,
|
|
};
|
|
}
|
|
}
|
|
} catch (error) {
|
|
return {
|
|
status: 'failed',
|
|
message: error?.description || error.message || 'Connection test failed',
|
|
};
|
|
}
|
|
}
|
|
|
|
async invokeMethod(methodName: string, ...args: any[]): Promise<QueryResult> {
|
|
const methodMap: Record<string, Function> = {
|
|
'discoverServices': this.discoverServices.bind(this)
|
|
};
|
|
|
|
const method = methodMap[methodName];
|
|
if (!method) {
|
|
throw new QueryError(
|
|
'Method not allowed',
|
|
`Method ${methodName} is not exposed by this plugin`,
|
|
{ allowedMethods: Object.keys(methodMap) }
|
|
);
|
|
}
|
|
|
|
return await method(...args);
|
|
}
|
|
|
|
private async discoverServices(sourceOptions: SourceOptions): Promise<GrpcService[]> {
|
|
try {
|
|
this.validateSourceOptionsForDiscovery(sourceOptions);
|
|
|
|
if (sourceOptions.proto_files === 'server_reflection') {
|
|
return await discoverServicesUsingReflection(sourceOptions);
|
|
} else {
|
|
return await discoverServicesUsingProtoFile(sourceOptions);
|
|
}
|
|
} catch (error: unknown) {
|
|
if (error instanceof GrpcOperationError) {
|
|
throw new QueryError('Query could not be completed', error.message, error.errorDetails);
|
|
}
|
|
|
|
const err = toError(error);
|
|
throw new QueryError('Query could not be completed', err.message, {
|
|
grpcCode: 0,
|
|
grpcStatus: 'UNKNOWN',
|
|
errorType: 'QueryError'
|
|
});
|
|
}
|
|
}
|
|
|
|
private async createGrpcClient(sourceOptions: SourceOptions, serviceName: string): Promise<GrpcClient> {
|
|
if (sourceOptions.proto_files === 'server_reflection') {
|
|
return await buildReflectionClient(sourceOptions, serviceName);
|
|
} else {
|
|
return await buildProtoFileClient(sourceOptions, serviceName);
|
|
}
|
|
}
|
|
|
|
private validateSourceOptionsForDiscovery(sourceOptions: SourceOptions): void {
|
|
if (!sourceOptions) {
|
|
throw new GrpcOperationError('Source options are required for service discovery');
|
|
}
|
|
}
|
|
|
|
private validateRequestData(queryOptions: QueryOptions): void {
|
|
const message = this.parseMessage(queryOptions.raw_message);
|
|
if (!message || typeof message !== 'object') {
|
|
throw new GrpcOperationError('Invalid message data. Please provide a valid JSON object in the Request tab.');
|
|
}
|
|
}
|
|
|
|
private parseMessage(raw_message?: string): Record<string, unknown> {
|
|
if (!raw_message || raw_message.trim() === '') {
|
|
return {};
|
|
}
|
|
|
|
try {
|
|
return JSON5.parse(raw_message);
|
|
} catch (error) {
|
|
const err = toError(error);
|
|
throw new GrpcOperationError(`Invalid JSON in request message: ${err.message}`, error);
|
|
}
|
|
}
|
|
|
|
private validateMethodExists(client: GrpcClient, queryOptions: QueryOptions): void {
|
|
const methodFunction = client[queryOptions.method];
|
|
if (!methodFunction || typeof methodFunction !== 'function') {
|
|
throw new GrpcOperationError(`Method ${queryOptions.method} not found in service ${queryOptions.service}`);
|
|
}
|
|
}
|
|
|
|
private async executeGrpcCall(client: GrpcClient, queryOptions: QueryOptions, sourceOptions: SourceOptions): Promise<Record<string, unknown>> {
|
|
const message = this.parseMessage(queryOptions.raw_message);
|
|
return executeGrpcMethod(client, queryOptions.method, message, sourceOptions, queryOptions);
|
|
}
|
|
|
|
authUrl(sourceOptions: SourceOptions): string {
|
|
return getAuthUrl(sourceOptions);
|
|
}
|
|
|
|
async refreshToken(
|
|
sourceOptions: SourceOptions,
|
|
error: Error,
|
|
userId: string,
|
|
isAppPublic: boolean
|
|
): Promise<Record<string, unknown>> {
|
|
return getRefreshedToken(sourceOptions, error, userId, isAppPublic);
|
|
}
|
|
}
|