mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-07-02 23:44:38 +08:00
✨ feat(rabbitmq): 新增 RabbitMQ 数据源连接与测试发消息支持
- 新增 RabbitMQ 管理 API 数据源实现,支持 vhost、queue、exchange 浏览与队列预览 - 统一消息发送弹窗,支持 Kafka Topic 与 RabbitMQ Queue 的测试发送命令生成 - 补齐连接表单、能力矩阵、SQL 方言、图标与前后端回归测试覆盖
This commit is contained in:
@@ -19,6 +19,7 @@ export const normalizeDriverType = (value: string): string => {
|
||||
if (normalized === 'qdrantdb' || normalized === 'qdrant-db') return 'qdrant';
|
||||
if (normalized === 'apache-iotdb' || normalized === 'apache_iotdb') return 'iotdb';
|
||||
if (normalized === 'apache-kafka' || normalized === 'apache_kafka') return 'kafka';
|
||||
if (normalized === 'rabbit-mq' || normalized === 'rabbit_mq') return 'rabbitmq';
|
||||
if (normalized === 'doris') return 'diros';
|
||||
if (
|
||||
normalized === 'open_gauss' ||
|
||||
|
||||
@@ -297,6 +297,19 @@ export const resolveConnectionConfigLayout = (
|
||||
],
|
||||
};
|
||||
}
|
||||
if (type === 'rabbitmq') {
|
||||
return {
|
||||
kind: 'generic-sql',
|
||||
sections: [
|
||||
'identity',
|
||||
'uri',
|
||||
'target',
|
||||
'service',
|
||||
'credentials',
|
||||
'databaseScope',
|
||||
],
|
||||
};
|
||||
}
|
||||
if (postgresCompatibleTypes.has(type)) {
|
||||
return {
|
||||
kind: 'postgres-compatible',
|
||||
|
||||
@@ -16,6 +16,7 @@ export const singleHostUriSchemesByType: Record<string, string[]> = {
|
||||
elasticsearch: ["http", "https"],
|
||||
chroma: ["http", "https", "chroma"],
|
||||
qdrant: ["http", "https", "qdrant"],
|
||||
rabbitmq: ["rabbitmq", "http", "https"],
|
||||
};
|
||||
|
||||
const normalizeConnectionType = (type: string) =>
|
||||
@@ -59,6 +60,7 @@ const sslSupportedTypes = new Set([
|
||||
"chroma",
|
||||
"qdrant",
|
||||
"kafka",
|
||||
"rabbitmq",
|
||||
]);
|
||||
|
||||
export const supportsSSLForType = (type: string) =>
|
||||
@@ -86,6 +88,7 @@ const sslCAPathSupportedTypes = new Set([
|
||||
"chroma",
|
||||
"qdrant",
|
||||
"kafka",
|
||||
"rabbitmq",
|
||||
]);
|
||||
|
||||
const sslClientCertificateSupportedTypes = new Set([
|
||||
@@ -107,6 +110,7 @@ const sslClientCertificateSupportedTypes = new Set([
|
||||
"mongodb",
|
||||
"redis",
|
||||
"kafka",
|
||||
"rabbitmq",
|
||||
]);
|
||||
|
||||
export const supportsSSLCAPathForType = (type: string) =>
|
||||
@@ -157,4 +161,5 @@ export const supportsConnectionParamsForType = (type: string) =>
|
||||
type === "elasticsearch" ||
|
||||
type === "chroma" ||
|
||||
type === "qdrant" ||
|
||||
type === "kafka";
|
||||
type === "kafka" ||
|
||||
type === "rabbitmq";
|
||||
|
||||
@@ -65,6 +65,7 @@ export const CONNECTION_TYPE_GROUPS: ConnectionTypeCatalogGroup[] = [
|
||||
label: '消息队列',
|
||||
items: [
|
||||
{ key: 'kafka', name: 'Kafka' },
|
||||
{ key: 'rabbitmq', name: 'RabbitMQ' },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -124,6 +125,8 @@ export const getConnectionTypeDefaultPort = (type: string): number => {
|
||||
return 6333;
|
||||
case 'kafka':
|
||||
return 9092;
|
||||
case 'rabbitmq':
|
||||
return 15672;
|
||||
case 'highgo':
|
||||
return 5866;
|
||||
case 'mariadb':
|
||||
@@ -158,6 +161,8 @@ export const getConnectionTypeHint = (type: string): string => {
|
||||
return 'Storage Group / Device / Timeseries';
|
||||
case 'kafka':
|
||||
return 'Broker / Topic / Consumer Group';
|
||||
case 'rabbitmq':
|
||||
return 'Management API / Virtual Host / Queue';
|
||||
case 'oceanbase':
|
||||
return 'MySQL / Oracle 租户';
|
||||
case 'goldendb':
|
||||
|
||||
@@ -173,11 +173,33 @@ describe('dataSourceCapabilities', () => {
|
||||
supportsCreateDatabase: false,
|
||||
supportsRenameDatabase: false,
|
||||
supportsDropDatabase: false,
|
||||
supportsMessagePublish: true,
|
||||
forceReadOnlyQueryResult: true,
|
||||
});
|
||||
expect(getDataSourceCapabilities({ type: 'custom', driver: 'apache-kafka' })).toMatchObject({
|
||||
type: 'kafka',
|
||||
supportsQueryEditor: true,
|
||||
supportsMessagePublish: true,
|
||||
forceReadOnlyQueryResult: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('treats RabbitMQ as a queryable messaging datasource with publish support', () => {
|
||||
expect(getDataSourceCapabilities({ type: 'rabbitmq' })).toMatchObject({
|
||||
type: 'rabbitmq',
|
||||
supportsQueryEditor: true,
|
||||
supportsSqlQueryExport: false,
|
||||
supportsCopyInsert: false,
|
||||
supportsCreateDatabase: false,
|
||||
supportsRenameDatabase: false,
|
||||
supportsDropDatabase: false,
|
||||
supportsMessagePublish: true,
|
||||
forceReadOnlyQueryResult: true,
|
||||
});
|
||||
expect(getDataSourceCapabilities({ type: 'custom', driver: 'rabbit-mq' })).toMatchObject({
|
||||
type: 'rabbitmq',
|
||||
supportsQueryEditor: true,
|
||||
supportsMessagePublish: true,
|
||||
forceReadOnlyQueryResult: true,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -42,6 +42,10 @@ const normalizeDataSourceToken = (raw: string): string => {
|
||||
case 'apache-kafka':
|
||||
case 'apache_kafka':
|
||||
return 'kafka';
|
||||
case 'rabbitmq':
|
||||
case 'rabbit-mq':
|
||||
case 'rabbit_mq':
|
||||
return 'rabbitmq';
|
||||
case 'intersystems':
|
||||
case 'intersystemsiris':
|
||||
case 'inter-systems':
|
||||
@@ -117,7 +121,8 @@ const COPY_INSERT_TYPES = new Set([
|
||||
]);
|
||||
|
||||
const QUERY_EDITOR_DISABLED_TYPES = new Set(['redis']);
|
||||
const FORCE_READ_ONLY_QUERY_TYPES = new Set(['tdengine', 'iotdb', 'clickhouse', 'kafka']);
|
||||
const FORCE_READ_ONLY_QUERY_TYPES = new Set(['tdengine', 'iotdb', 'clickhouse', 'kafka', 'rabbitmq']);
|
||||
const MESSAGE_PUBLISH_TYPES = new Set(['kafka', 'rabbitmq']);
|
||||
const MANUAL_TOTAL_COUNT_TYPES = new Set(['duckdb', 'oracle']);
|
||||
const APPROXIMATE_TABLE_COUNT_TYPES = new Set(['duckdb', 'oracle']);
|
||||
const APPROXIMATE_TOTAL_PAGE_TYPES = new Set(['duckdb']);
|
||||
@@ -130,6 +135,7 @@ export type DataSourceCapabilities = {
|
||||
supportsCreateDatabase: boolean;
|
||||
supportsRenameDatabase: boolean;
|
||||
supportsDropDatabase: boolean;
|
||||
supportsMessagePublish: boolean;
|
||||
forceReadOnlyQueryResult: boolean;
|
||||
preferManualTotalCount: boolean;
|
||||
supportsApproximateTableCount: boolean;
|
||||
@@ -191,6 +197,7 @@ export const getDataSourceCapabilities = (config: ConnectionLike): DataSourceCap
|
||||
supportsCreateDatabase: CREATE_DATABASE_TYPES.has(type),
|
||||
supportsRenameDatabase: RENAME_DATABASE_TYPES.has(type),
|
||||
supportsDropDatabase: DROP_DATABASE_TYPES.has(type),
|
||||
supportsMessagePublish: MESSAGE_PUBLISH_TYPES.has(type),
|
||||
forceReadOnlyQueryResult: FORCE_READ_ONLY_QUERY_TYPES.has(type),
|
||||
preferManualTotalCount: MANUAL_TOTAL_COUNT_TYPES.has(type),
|
||||
supportsApproximateTableCount: APPROXIMATE_TABLE_COUNT_TYPES.has(type),
|
||||
|
||||
100
frontend/src/utils/messagePublish.test.ts
Normal file
100
frontend/src/utils/messagePublish.test.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
buildMessagePublishCommand,
|
||||
createDefaultMessagePublishDraft,
|
||||
} from './messagePublish';
|
||||
|
||||
describe('messagePublish', () => {
|
||||
it('builds a Kafka publish JSON command from JSON payload inputs', () => {
|
||||
const result = buildMessagePublishCommand(
|
||||
{ type: 'kafka' },
|
||||
{
|
||||
destination: 'orders.events',
|
||||
keyMode: 'json',
|
||||
key: '{"tenant":"a"}',
|
||||
bodyMode: 'json',
|
||||
body: '{"id":1,"event":"created"}',
|
||||
headers: '{"x-env":"dev"}',
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.transportLabel).toBe('Kafka Topic');
|
||||
expect(result.destinationLabel).toBe('orders.events');
|
||||
expect(result.commandText).toContain('"publish": "orders.events"');
|
||||
expect(result.commandText).toContain('"tenant": "a"');
|
||||
expect(result.commandText).toContain('"id": 1');
|
||||
expect(result.commandText).toContain('"x-env": "dev"');
|
||||
});
|
||||
|
||||
it('keeps Kafka text payloads as plain strings', () => {
|
||||
const result = buildMessagePublishCommand(
|
||||
{ type: 'kafka' },
|
||||
{
|
||||
destination: 'logs.app',
|
||||
keyMode: 'text',
|
||||
key: 'tenant-a',
|
||||
bodyMode: 'text',
|
||||
body: 'hello gonavi',
|
||||
headers: '',
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.commandText).toContain('"key": "tenant-a"');
|
||||
expect(result.commandText).toContain('"value": "hello gonavi"');
|
||||
});
|
||||
|
||||
it('rejects non-object Kafka headers', () => {
|
||||
expect(() => buildMessagePublishCommand(
|
||||
{ type: 'kafka' },
|
||||
{
|
||||
destination: 'logs.app',
|
||||
bodyMode: 'json',
|
||||
body: '{"ok":true}',
|
||||
headers: '["bad"]',
|
||||
},
|
||||
)).toThrow('Headers 必须是 JSON 对象');
|
||||
});
|
||||
|
||||
it('seeds Kafka default publish draft with a JSON body example', () => {
|
||||
expect(createDefaultMessagePublishDraft({ type: 'kafka' }, 'orders.events')).toMatchObject({
|
||||
destination: 'orders.events',
|
||||
keyMode: 'text',
|
||||
bodyMode: 'json',
|
||||
});
|
||||
});
|
||||
|
||||
it('builds a RabbitMQ publish JSON command with routing and properties', () => {
|
||||
const result = buildMessagePublishCommand(
|
||||
{ type: 'rabbitmq', connectionParams: 'defaultQueue=orders.queue&exchange=events.topic' },
|
||||
{
|
||||
destination: 'orders.queue',
|
||||
exchange: '',
|
||||
routingKey: '',
|
||||
bodyMode: 'json',
|
||||
body: '{"id":1,"event":"created"}',
|
||||
headers: '{"x-env":"dev"}',
|
||||
properties: '{"content_type":"application/json"}',
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.transportLabel).toBe('RabbitMQ Queue');
|
||||
expect(result.destinationLabel).toBe('orders.queue');
|
||||
expect(result.commandText).toContain('"publish": "orders.queue"');
|
||||
expect(result.commandText).toContain('"exchange": "events.topic"');
|
||||
expect(result.commandText).toContain('"routing_key": "orders.queue"');
|
||||
expect(result.commandText).toContain('"content_type": "application/json"');
|
||||
});
|
||||
|
||||
it('seeds RabbitMQ default publish draft with defaultQueue and exchange', () => {
|
||||
expect(createDefaultMessagePublishDraft(
|
||||
{ type: 'rabbitmq', connectionParams: 'defaultQueue=orders.queue&exchange=events.topic' },
|
||||
'',
|
||||
)).toMatchObject({
|
||||
destination: 'orders.queue',
|
||||
exchange: 'events.topic',
|
||||
routingKey: 'orders.queue',
|
||||
bodyMode: 'json',
|
||||
});
|
||||
});
|
||||
});
|
||||
274
frontend/src/utils/messagePublish.ts
Normal file
274
frontend/src/utils/messagePublish.ts
Normal file
@@ -0,0 +1,274 @@
|
||||
import { resolveDataSourceType } from './dataSourceCapabilities';
|
||||
|
||||
type ConnectionLike = {
|
||||
type?: string;
|
||||
driver?: string;
|
||||
oceanBaseProtocol?: string;
|
||||
database?: string;
|
||||
uri?: string;
|
||||
connectionParams?: string;
|
||||
} | null | undefined;
|
||||
|
||||
export type MessagePublishValueMode = 'text' | 'json';
|
||||
|
||||
export type MessagePublishDraft = {
|
||||
destination: string;
|
||||
exchange?: string;
|
||||
routingKey?: string;
|
||||
keyMode?: MessagePublishValueMode;
|
||||
key?: string;
|
||||
bodyMode?: MessagePublishValueMode;
|
||||
body: string;
|
||||
headers?: string;
|
||||
properties?: string;
|
||||
};
|
||||
|
||||
export type MessagePublishCommand = {
|
||||
commandText: string;
|
||||
destinationLabel: string;
|
||||
transportLabel: string;
|
||||
};
|
||||
|
||||
export type MessagePublishPresentation = {
|
||||
transportLabel: string;
|
||||
destinationLabel: string;
|
||||
destinationPlaceholder: string;
|
||||
destinationRequiredMessage: string;
|
||||
alertMessage: string;
|
||||
successHint: string;
|
||||
showKey: boolean;
|
||||
showExchange: boolean;
|
||||
showRoutingKey: boolean;
|
||||
showProperties: boolean;
|
||||
};
|
||||
|
||||
const normalizeMode = (value: unknown, fallback: MessagePublishValueMode): MessagePublishValueMode => {
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
if (normalized === 'text') return 'text';
|
||||
if (normalized === 'json') return 'json';
|
||||
return fallback;
|
||||
};
|
||||
|
||||
const parseRequiredPayload = (
|
||||
rawValue: unknown,
|
||||
mode: MessagePublishValueMode,
|
||||
fieldLabel: string,
|
||||
): string | number | boolean | Record<string, any> | Array<any> => {
|
||||
const text = String(rawValue ?? '');
|
||||
if (!text.trim()) {
|
||||
throw new Error(`请输入${fieldLabel}`);
|
||||
}
|
||||
if (mode === 'text') {
|
||||
return text;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
} catch (error: any) {
|
||||
throw new Error(`${fieldLabel}不是合法 JSON:${error?.message || String(error)}`);
|
||||
}
|
||||
};
|
||||
|
||||
const parseOptionalPayload = (
|
||||
rawValue: unknown,
|
||||
mode: MessagePublishValueMode,
|
||||
fieldLabel: string,
|
||||
): string | number | boolean | Record<string, any> | Array<any> | undefined => {
|
||||
const text = String(rawValue ?? '');
|
||||
if (!text.trim()) {
|
||||
return undefined;
|
||||
}
|
||||
return parseRequiredPayload(text, mode, fieldLabel);
|
||||
};
|
||||
|
||||
const parseOptionalJSONObject = (
|
||||
rawValue: unknown,
|
||||
fieldLabel: string,
|
||||
): Record<string, any> | undefined => {
|
||||
const text = String(rawValue ?? '');
|
||||
if (!text.trim()) {
|
||||
return undefined;
|
||||
}
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(text);
|
||||
} catch (error: any) {
|
||||
throw new Error(`${fieldLabel}不是合法 JSON:${error?.message || String(error)}`);
|
||||
}
|
||||
if (!parsed || Array.isArray(parsed) || typeof parsed !== 'object') {
|
||||
throw new Error(`${fieldLabel} 必须是 JSON 对象`);
|
||||
}
|
||||
return parsed as Record<string, any>;
|
||||
};
|
||||
|
||||
const mergeSearchParams = (target: URLSearchParams, sourceText: unknown) => {
|
||||
const text = String(sourceText ?? '').trim();
|
||||
if (!text) return;
|
||||
const raw = text.includes('?') ? text.slice(text.indexOf('?') + 1) : text;
|
||||
const params = new URLSearchParams(raw.replace(/^\?/, ''));
|
||||
params.forEach((value, key) => {
|
||||
if (String(key || '').trim()) {
|
||||
target.set(key, value);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const resolveConnectionParams = (config: ConnectionLike): URLSearchParams => {
|
||||
const params = new URLSearchParams();
|
||||
if (!config) return params;
|
||||
mergeSearchParams(params, config.uri);
|
||||
mergeSearchParams(params, config.connectionParams);
|
||||
return params;
|
||||
};
|
||||
|
||||
const normalizeRabbitMQExchange = (value: unknown): string => {
|
||||
const normalized = String(value ?? '').trim();
|
||||
if (normalized === 'amq.default' || normalized === '(default)') {
|
||||
return '';
|
||||
}
|
||||
return normalized;
|
||||
};
|
||||
|
||||
const resolveDefaultDestination = (config: ConnectionLike, explicitDestination: string): string => {
|
||||
const destination = String(explicitDestination || '').trim();
|
||||
if (destination) return destination;
|
||||
|
||||
const resolvedType = resolveDataSourceType(config as any);
|
||||
const params = resolveConnectionParams(config);
|
||||
|
||||
if (resolvedType === 'kafka') {
|
||||
return String(config?.database || '').trim();
|
||||
}
|
||||
if (resolvedType === 'rabbitmq') {
|
||||
return String(params.get('defaultQueue') || params.get('queue') || '').trim();
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
export const getMessagePublishPresentation = (
|
||||
config: ConnectionLike,
|
||||
): MessagePublishPresentation => {
|
||||
const resolvedType = resolveDataSourceType(config as any);
|
||||
|
||||
if (resolvedType === 'rabbitmq') {
|
||||
return {
|
||||
transportLabel: 'RabbitMQ Queue',
|
||||
destinationLabel: 'Queue',
|
||||
destinationPlaceholder: '例如:orders.queue',
|
||||
destinationRequiredMessage: '请输入 Queue',
|
||||
alertMessage: '当前表单会自动拼装 RabbitMQ publish JSON 命令,并通过 Management API 执行测试发送。',
|
||||
successHint: '留空 Exchange 时会使用默认交换机并按 Queue 名作为 routing key。',
|
||||
showKey: false,
|
||||
showExchange: true,
|
||||
showRoutingKey: true,
|
||||
showProperties: true,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
transportLabel: 'Kafka Topic',
|
||||
destinationLabel: 'Topic',
|
||||
destinationPlaceholder: '例如:orders.events',
|
||||
destinationRequiredMessage: '请输入 Topic',
|
||||
alertMessage: '当前表单会自动拼装 Kafka publish JSON 命令,并直接调用后端执行测试发送。',
|
||||
successHint: 'Headers 会作为 Kafka Record Headers 一并发送。',
|
||||
showKey: true,
|
||||
showExchange: false,
|
||||
showRoutingKey: false,
|
||||
showProperties: false,
|
||||
};
|
||||
};
|
||||
|
||||
export const createDefaultMessagePublishDraft = (
|
||||
config: ConnectionLike,
|
||||
destination = '',
|
||||
): MessagePublishDraft => {
|
||||
const resolvedType = resolveDataSourceType(config as any);
|
||||
const resolvedDestination = resolveDefaultDestination(config, destination);
|
||||
const params = resolveConnectionParams(config);
|
||||
|
||||
if (resolvedType === 'rabbitmq') {
|
||||
return {
|
||||
destination: resolvedDestination,
|
||||
exchange: normalizeRabbitMQExchange(params.get('defaultExchange') || params.get('exchange') || ''),
|
||||
routingKey: resolvedDestination,
|
||||
bodyMode: 'json',
|
||||
body: '{\n "event": "test",\n "source": "gonavi"\n}',
|
||||
headers: '{\n "x-source": "gonavi"\n}',
|
||||
properties: '{\n "content_type": "application/json"\n}',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
destination: resolvedDestination,
|
||||
keyMode: 'text',
|
||||
key: '',
|
||||
bodyMode: 'json',
|
||||
body: '{\n "event": "test",\n "source": "gonavi"\n}',
|
||||
headers: '{\n "x-source": "gonavi"\n}',
|
||||
};
|
||||
};
|
||||
|
||||
export const buildMessagePublishCommand = (
|
||||
config: ConnectionLike,
|
||||
draft: MessagePublishDraft,
|
||||
): MessagePublishCommand => {
|
||||
const resolvedType = resolveDataSourceType(config as any);
|
||||
const destination = String(draft.destination || '').trim();
|
||||
if (!destination) {
|
||||
throw new Error('请输入目标 Topic / Queue');
|
||||
}
|
||||
|
||||
if (resolvedType === 'rabbitmq') {
|
||||
const params = resolveConnectionParams(config);
|
||||
const bodyMode = normalizeMode(draft.bodyMode, 'json');
|
||||
const command: Record<string, unknown> = {
|
||||
publish: destination,
|
||||
payload: parseRequiredPayload(draft.body, bodyMode, '消息体'),
|
||||
exchange: normalizeRabbitMQExchange(draft.exchange || params.get('defaultExchange') || params.get('exchange') || ''),
|
||||
routing_key: String(draft.routingKey || '').trim() || destination,
|
||||
};
|
||||
|
||||
const headers = parseOptionalJSONObject(draft.headers, 'Headers');
|
||||
if (headers && Object.keys(headers).length > 0) {
|
||||
command.headers = headers;
|
||||
}
|
||||
|
||||
const properties = parseOptionalJSONObject(draft.properties, 'Properties');
|
||||
if (properties && Object.keys(properties).length > 0) {
|
||||
command.properties = properties;
|
||||
}
|
||||
|
||||
return {
|
||||
commandText: JSON.stringify(command, null, 2),
|
||||
destinationLabel: destination,
|
||||
transportLabel: 'RabbitMQ Queue',
|
||||
};
|
||||
}
|
||||
|
||||
if (resolvedType === 'kafka') {
|
||||
const keyMode = normalizeMode(draft.keyMode, 'text');
|
||||
const bodyMode = normalizeMode(draft.bodyMode, 'json');
|
||||
const command: Record<string, unknown> = {
|
||||
publish: destination,
|
||||
value: parseRequiredPayload(draft.body, bodyMode, '消息体'),
|
||||
};
|
||||
|
||||
const keyPayload = parseOptionalPayload(draft.key, keyMode, '消息 Key');
|
||||
if (keyPayload !== undefined) {
|
||||
command.key = keyPayload;
|
||||
}
|
||||
|
||||
const headers = parseOptionalJSONObject(draft.headers, 'Headers');
|
||||
if (headers && Object.keys(headers).length > 0) {
|
||||
command.headers = headers;
|
||||
}
|
||||
|
||||
return {
|
||||
commandText: JSON.stringify(command, null, 2),
|
||||
destinationLabel: destination,
|
||||
transportLabel: 'Kafka Topic',
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error(`当前数据源暂不支持测试发送消息:${resolvedType || 'unknown'}`);
|
||||
};
|
||||
@@ -10,4 +10,8 @@ describe('buildTableSelectQuery', () => {
|
||||
it('adds a preview limit for Kafka topic browsing', () => {
|
||||
expect(buildTableSelectQuery('kafka', 'logs.app-1')).toBe('SELECT * FROM "logs.app-1" LIMIT 100;');
|
||||
});
|
||||
|
||||
it('adds a preview limit for RabbitMQ queue browsing', () => {
|
||||
expect(buildTableSelectQuery('rabbitmq', 'orders.events.v1')).toBe('SELECT * FROM "orders.events.v1" LIMIT 100;');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ export const buildTableSelectQuery = (dbType: string, tableName: string): string
|
||||
if (!normalizedTableName) {
|
||||
return 'SELECT * FROM ';
|
||||
}
|
||||
if (String(dbType || '').trim().toLowerCase() === 'kafka') {
|
||||
if (['kafka', 'rabbitmq'].includes(String(dbType || '').trim().toLowerCase())) {
|
||||
return `SELECT * FROM ${quoteQualifiedIdent(dbType, normalizedTableName)} LIMIT 100;`;
|
||||
}
|
||||
return `SELECT * FROM ${quoteQualifiedIdent(dbType, normalizedTableName)};`;
|
||||
|
||||
@@ -64,6 +64,11 @@ describe('quoteQualifiedIdent', () => {
|
||||
.toBe('"logs.app-1"');
|
||||
});
|
||||
|
||||
it('keeps RabbitMQ queue names as one quoted identifier', () => {
|
||||
expect(quoteQualifiedIdent('rabbitmq', 'orders.events.v1'))
|
||||
.toBe('"orders.events.v1"');
|
||||
});
|
||||
|
||||
it('quotes GoldenDB identifiers with MySQL-style backticks', () => {
|
||||
expect(quoteQualifiedIdent('goldendb', 'ledger.entries'))
|
||||
.toBe('`ledger`.`entries`');
|
||||
|
||||
@@ -54,7 +54,7 @@ export const quoteIdentPart = (dbType: string, ident: string) => {
|
||||
export const quoteQualifiedIdent = (dbType: string, ident: string) => {
|
||||
const raw = (ident || '').trim();
|
||||
if (!raw) return raw;
|
||||
if ((dbType || '').trim().toLowerCase() === 'kafka') {
|
||||
if (['kafka', 'rabbitmq'].includes((dbType || '').trim().toLowerCase())) {
|
||||
return quoteIdentPart(dbType, raw);
|
||||
}
|
||||
const parts = splitQualifiedNameSegments(raw).filter(Boolean);
|
||||
|
||||
@@ -40,6 +40,8 @@ describe('sqlDialect', () => {
|
||||
expect(resolveSqlDialect('custom', 'apache_iotdb')).toBe('iotdb');
|
||||
expect(resolveSqlDialect('Apache-Kafka')).toBe('kafka');
|
||||
expect(resolveSqlDialect('custom', 'apache_kafka')).toBe('kafka');
|
||||
expect(resolveSqlDialect('Rabbit-MQ')).toBe('rabbitmq');
|
||||
expect(resolveSqlDialect('custom', 'rabbit_mq')).toBe('rabbitmq');
|
||||
expect(resolveSqlDialect('OceanBase', '', { oceanBaseProtocol: 'oracle' })).toBe('oracle');
|
||||
expect(resolveSqlDialect('custom', 'oceanbase', { oceanBaseProtocol: 'oracle' })).toBe('oracle');
|
||||
expect(isMysqlFamilyDialect('mariadb')).toBe(true);
|
||||
@@ -78,6 +80,11 @@ describe('sqlDialect', () => {
|
||||
expect(resolveSqlKeywords('kafka')).not.toEqual(expect.arrayContaining(['ALIGN BY DEVICE', 'AUTO_INCREMENT']));
|
||||
});
|
||||
|
||||
it('resolves RabbitMQ completion keywords for queue and exchange discovery', () => {
|
||||
expect(resolveSqlKeywords('rabbitmq')).toEqual(expect.arrayContaining(['SHOW VHOSTS', 'SHOW QUEUES', 'SHOW EXCHANGES', 'DESCRIBE QUEUE']));
|
||||
expect(resolveSqlKeywords('rabbitmq')).not.toEqual(expect.arrayContaining(['ALIGN BY DEVICE', 'AUTO_INCREMENT']));
|
||||
});
|
||||
|
||||
it('resolves GaussDB completion keywords and functions as a PostgreSQL-like dialect', () => {
|
||||
expect(resolveSqlKeywords('gaussdb')).toEqual(expect.arrayContaining(['RETURNING', 'SERIAL', 'JSONB']));
|
||||
expect(names(resolveSqlFunctions('gaussdb'))).toEqual(expect.arrayContaining(['STRING_AGG', 'TO_CHAR', 'CURRENT_DATABASE']));
|
||||
|
||||
@@ -30,6 +30,7 @@ export type SqlDialect =
|
||||
| 'tdengine'
|
||||
| 'iotdb'
|
||||
| 'kafka'
|
||||
| 'rabbitmq'
|
||||
| 'mongodb'
|
||||
| 'redis'
|
||||
| 'elasticsearch'
|
||||
@@ -140,6 +141,10 @@ export const resolveSqlDialect = (
|
||||
case 'apache-kafka':
|
||||
case 'apache_kafka':
|
||||
return 'kafka';
|
||||
case 'rabbitmq':
|
||||
case 'rabbit-mq':
|
||||
case 'rabbit_mq':
|
||||
return 'rabbitmq';
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -165,6 +170,7 @@ export const resolveSqlDialect = (
|
||||
if (source.includes('tdengine')) return 'tdengine';
|
||||
if (source.includes('iotdb')) return 'iotdb';
|
||||
if (source.includes('kafka')) return 'kafka';
|
||||
if (source.includes('rabbitmq') || source.includes('rabbit-mq') || source.includes('rabbit_mq')) return 'rabbitmq';
|
||||
if (source.includes('sqlserver') || source.includes('mssql')) return 'sqlserver';
|
||||
if (source.includes('iris') || source.includes('intersystems')) return 'iris';
|
||||
if (source.includes('elastic')) return 'elasticsearch';
|
||||
@@ -628,6 +634,18 @@ const KAFKA_KEYWORDS = [
|
||||
'OFFSET',
|
||||
];
|
||||
|
||||
const RABBITMQ_KEYWORDS = [
|
||||
'SHOW VHOSTS',
|
||||
'SHOW QUEUES',
|
||||
'SHOW EXCHANGES',
|
||||
'DESCRIBE QUEUE',
|
||||
'DESCRIBE EXCHANGE',
|
||||
'CONSUME',
|
||||
'FROM',
|
||||
'LIMIT',
|
||||
'OFFSET',
|
||||
];
|
||||
|
||||
export const resolveSqlKeywords = (dbType: string): string[] => {
|
||||
const dialect = resolveSqlDialect(dbType);
|
||||
if (dialect === 'starrocks') return unique([...COMMON_KEYWORDS, ...MYSQL_KEYWORDS, ...STARROCKS_KEYWORDS]);
|
||||
@@ -641,6 +659,7 @@ export const resolveSqlKeywords = (dbType: string): string[] => {
|
||||
if (dialect === 'tdengine') return unique([...COMMON_KEYWORDS, ...TDENGINE_KEYWORDS]);
|
||||
if (dialect === 'iotdb') return unique([...COMMON_KEYWORDS, ...IOTDB_KEYWORDS]);
|
||||
if (dialect === 'kafka') return unique([...COMMON_KEYWORDS, ...KAFKA_KEYWORDS]);
|
||||
if (dialect === 'rabbitmq') return unique([...COMMON_KEYWORDS, ...RABBITMQ_KEYWORDS]);
|
||||
return COMMON_KEYWORDS;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user