feat(plugin): expose a read-only schema metadata host API
This commit is contained in:
1 parent
2f960f104c
commit
d5a05a9884
40 files changed
+1445
-86
No files matched your search
@@ -9,6 +9,7 @@ import QueryLoadingState from "@/components/common/QueryLoadingState.vue";
|
||||
import JsonTree from "@/components/common/JsonTree.vue";
|
||||
import RedisJsonEditor from "@/components/redis/RedisJsonEditor.vue";
|
||||
import * as api from "@/lib/backend/api";
|
||||
import type { MeilisearchIndexSettings } from "@/lib/backend/tauri";
|
||||
import { useToast } from "@/composables/useToast";
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import { useQueryStore } from "@/stores/queryStore";
|
||||
@@ -28,7 +29,7 @@ const connectionStore = useConnectionStore();
|
||||
const queryStore = useQueryStore();
|
||||
|
||||
const settingsText = ref("");
|
||||
const settingsValue = ref<Record<string, any>>({});
|
||||
const settingsValue = ref<MeilisearchIndexSettings>({});
|
||||
const loading = ref(false);
|
||||
const error = ref("");
|
||||
const editMode = ref(false);
|
||||
@@ -82,9 +83,9 @@ async function saveSettings() {
|
||||
isSaving.value = true;
|
||||
saveError.value = "";
|
||||
try {
|
||||
await api.meilisearchUpdateIndexSettings(props.connectionId, props.index, parsed as Record<string, any>);
|
||||
await api.meilisearchUpdateIndexSettings(props.connectionId, props.index, parsed as Record<string, unknown>);
|
||||
toast(t("meilisearch.settingsSaved"));
|
||||
settingsValue.value = parsed as Record<string, any>;
|
||||
settingsValue.value = parsed as MeilisearchIndexSettings;
|
||||
settingsText.value = JSON.stringify(parsed, null, 2);
|
||||
editMode.value = false;
|
||||
} catch (e: any) {
|
||||
|
||||
@@ -383,6 +383,7 @@ function createBridge() {
|
||||
// bridge; the backend owns EXPLAIN generation, the timeout, and the plan cap.
|
||||
getPlanCapabilities: (connectionId) => api.getPluginPlanCapabilities(connectionId),
|
||||
explainPlan: (request) => api.getPluginEstimatedPlan(request),
|
||||
getTableMetadata: (context) => api.getPluginTableMetadata(context),
|
||||
closeTab: () => emit("closeTab"),
|
||||
saveFile: (_pluginId, request, data) => savePluginFile(request, data),
|
||||
downloadFile: isTauriRuntime() ? downloadPluginFile : undefined,
|
||||
|
||||
@@ -214,6 +214,7 @@ export const getObjectSource = forward("getObjectSource");
|
||||
export const getEventInfo = forward("getEventInfo");
|
||||
export const getCustomTypeDetails = forward("getCustomTypeDetails");
|
||||
export const getColumns = forward("getColumns");
|
||||
export const getPluginTableMetadata = forward("getPluginTableMetadata");
|
||||
export const getAllColumns = forward("getAllColumns");
|
||||
export const getSqlServerColumnMetadata = forward("getSqlServerColumnMetadata");
|
||||
export const listDataTypes = forward("listDataTypes");
|
||||
|
||||
@@ -190,10 +190,12 @@ import type {
|
||||
PromptTemplate,
|
||||
SshPromptResolution,
|
||||
MeilisearchIndexOverview,
|
||||
MeilisearchIndexSettings,
|
||||
ElasticsearchIndexMetadataKind,
|
||||
ElasticsearchDeleteByQueryResult,
|
||||
} from "@/lib/backend/tauri";
|
||||
import type { QueryEditability } from "@/lib/sql/sqlAnalysis";
|
||||
import type { PluginTableMetadata, PluginTableMetadataRequest } from "@/types/pluginSchemaMetadata";
|
||||
import type { CsvQuoteMode } from "@/lib/export/csvQuoteMode";
|
||||
import type { MigrationPreflight, MigrationReport } from "./migration";
|
||||
export type { MigrationPreflight, MigrationReport } from "./migration";
|
||||
@@ -1166,6 +1168,10 @@ export async function getColumns(connectionId: string, database: string, schema:
|
||||
return get(`/api/schema/columns?${qs({ connection_id: connectionId, database, schema, table, catalog, client_session_id: clientSessionId })}`);
|
||||
}
|
||||
|
||||
export async function getPluginTableMetadata(request: PluginTableMetadataRequest): Promise<PluginTableMetadata> {
|
||||
return post("/api/plugin/table-metadata", request);
|
||||
}
|
||||
|
||||
export async function getSqlServerColumnMetadata(connectionId: string, database: string, schema: string, table: string): Promise<SqlServerColumnMetadata[]> {
|
||||
return get(`/api/schema/sqlserver/column-metadata?${qs({ connection_id: connectionId, database, schema, table })}`);
|
||||
}
|
||||
@@ -4928,14 +4934,14 @@ export async function meilisearchGetDocument(connectionId: string, index: string
|
||||
});
|
||||
}
|
||||
|
||||
export async function meilisearchGetIndexSettings(connectionId: string, index: string): Promise<Record<string, any>> {
|
||||
export async function meilisearchGetIndexSettings(connectionId: string, index: string): Promise<MeilisearchIndexSettings> {
|
||||
return post("/api/document-store/meilisearch/settings/get", {
|
||||
connectionId,
|
||||
index,
|
||||
});
|
||||
}
|
||||
|
||||
export async function meilisearchUpdateIndexSettings(connectionId: string, index: string, settings: Record<string, any>): Promise<void> {
|
||||
export async function meilisearchUpdateIndexSettings(connectionId: string, index: string, settings: Record<string, unknown>): Promise<void> {
|
||||
return post("/api/document-store/meilisearch/settings/update", {
|
||||
connectionId,
|
||||
index,
|
||||
@@ -4943,7 +4949,7 @@ export async function meilisearchUpdateIndexSettings(connectionId: string, index
|
||||
});
|
||||
}
|
||||
|
||||
export async function meilisearchGetIndexStats(connectionId: string, index: string): Promise<{ numberOfDocuments: number; isIndexing: boolean; fieldDistribution: Record<string, number> } & Record<string, any>> {
|
||||
export async function meilisearchGetIndexStats(connectionId: string, index: string): Promise<{ numberOfDocuments: number; isIndexing: boolean; fieldDistribution: Record<string, number> } & Record<string, unknown>> {
|
||||
return post("/api/document-store/meilisearch/stats", {
|
||||
connectionId,
|
||||
index,
|
||||
|
||||
@@ -24,6 +24,7 @@ import { collectBrowserSupportInfo } from "@/lib/app/supportInfo";
|
||||
// Re-exported below so the HTTP transport shares one definition; imported here
|
||||
// for this module's own signatures (a re-export does not bind local names).
|
||||
import type { PluginPlanCapabilities, PluginPlanRequest, PluginPlanResult } from "@/types/pluginPlan";
|
||||
import type { PluginTableMetadata, PluginTableMetadataRequest } from "@/types/pluginSchemaMetadata";
|
||||
|
||||
function invoke<T>(command: string, args?: Record<string, unknown>): Promise<T> {
|
||||
assertUpdateAllowsCommand(command);
|
||||
@@ -832,6 +833,7 @@ export async function saveMaxRetries(maxRetries: number): Promise<void> {
|
||||
export type { OpenTabsStatePayload, PersistedEditorGroup } from "@/lib/app/openTabsPersistence";
|
||||
/** Shared with `@/lib/plugins/pluginHostBridge`; re-exported so the HTTP transport reuses one definition. */
|
||||
export type { PluginPlanCapabilities, PluginPlanRequest, PluginPlanResult } from "@/types/pluginPlan";
|
||||
export type { PluginColumnMetadata, PluginMetadataFieldAvailability, PluginMetadataFieldCapabilities, PluginTableMetadata, PluginTableMetadataRequest } from "@/types/pluginSchemaMetadata";
|
||||
import type { OpenTabsStatePayload } from "@/lib/app/openTabsPersistence";
|
||||
import { uuid } from "@/lib/common/utils";
|
||||
|
||||
@@ -1540,6 +1542,10 @@ export async function getColumns(connectionId: string, database: string, schema:
|
||||
});
|
||||
}
|
||||
|
||||
export async function getPluginTableMetadata(request: PluginTableMetadataRequest): Promise<PluginTableMetadata> {
|
||||
return invoke("get_plugin_table_metadata", { request });
|
||||
}
|
||||
|
||||
export async function getSqlServerColumnMetadata(connectionId: string, database: string, schema: string, table: string): Promise<SqlServerColumnMetadata[]> {
|
||||
return invoke("get_sqlserver_column_metadata", {
|
||||
connectionId,
|
||||
@@ -4765,14 +4771,14 @@ export async function meilisearchGetDocument(connectionId: string, index: string
|
||||
});
|
||||
}
|
||||
|
||||
export async function meilisearchGetIndexSettings(connectionId: string, index: string): Promise<Record<string, any>> {
|
||||
export async function meilisearchGetIndexSettings(connectionId: string, index: string): Promise<MeilisearchIndexSettings> {
|
||||
return invoke("meilisearch_get_index_settings", {
|
||||
connectionId,
|
||||
index,
|
||||
});
|
||||
}
|
||||
|
||||
export async function meilisearchUpdateIndexSettings(connectionId: string, index: string, settings: Record<string, any>): Promise<void> {
|
||||
export async function meilisearchUpdateIndexSettings(connectionId: string, index: string, settings: Record<string, unknown>): Promise<void> {
|
||||
return invoke("meilisearch_update_index_settings", {
|
||||
connectionId,
|
||||
index,
|
||||
@@ -4780,13 +4786,20 @@ export async function meilisearchUpdateIndexSettings(connectionId: string, index
|
||||
});
|
||||
}
|
||||
|
||||
export async function meilisearchGetIndexStats(connectionId: string, index: string): Promise<{ numberOfDocuments: number; isIndexing: boolean; fieldDistribution: Record<string, number> } & Record<string, any>> {
|
||||
export async function meilisearchGetIndexStats(connectionId: string, index: string): Promise<{ numberOfDocuments: number; isIndexing: boolean; fieldDistribution: Record<string, number> } & Record<string, unknown>> {
|
||||
return invoke("meilisearch_get_index_stats", {
|
||||
connectionId,
|
||||
index,
|
||||
});
|
||||
}
|
||||
|
||||
export interface MeilisearchIndexSettings {
|
||||
[key: string]: unknown;
|
||||
pagination?: {
|
||||
maxTotalHits?: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface MeilisearchIndexOverview {
|
||||
uid: string;
|
||||
primaryKey: string | null;
|
||||
|
||||
@@ -567,6 +567,95 @@ describe("PluginHostBridge", () => {
|
||||
expect(messages[2]).toMatchObject({ id: "legacy", error: "Host plan API is unavailable" });
|
||||
});
|
||||
|
||||
it("requires host.schema:read, normalizes table identity, and serves narrow metadata", async () => {
|
||||
const messages: unknown[] = [];
|
||||
const target = { postMessage: (message: unknown) => messages.push(message) } as unknown as Window;
|
||||
const getTableMetadata = vi.fn().mockResolvedValue({
|
||||
columns: [{ name: "id", dataType: "integer", nullable: false }],
|
||||
fieldCapabilities: { length: "unknown", precision: "unknown", scale: "unknown", default: "unknown" },
|
||||
});
|
||||
const base = { invoke: vi.fn(), notify: vi.fn(), sendBinary: vi.fn(), readAsset: vi.fn(), getTableMetadata };
|
||||
|
||||
const denied = new PluginHostBridge(plugin(), workbench, {}, () => target, base);
|
||||
planCall(denied, target, "host.getTableMetadata", { connectionId: "c1", table: "users" }, "denied");
|
||||
await vi.waitFor(() => expect(messages).toHaveLength(1));
|
||||
expect(messages[0]).toMatchObject({ id: "denied", error: "Plugin has not declared permission 'host.schema:read'" });
|
||||
expect(getTableMetadata).not.toHaveBeenCalled();
|
||||
|
||||
const allowed = new PluginHostBridge(plugin(["host.schema:read"]), workbench, {}, () => target, base);
|
||||
allowed.sendInit();
|
||||
planCall(allowed, target, "host.getTableMetadata", { connectionId: " c1 ", database: " app ", schema: " ", table: " users " }, "metadata");
|
||||
await vi.waitFor(() => expect(messages).toHaveLength(3));
|
||||
|
||||
expect(getTableMetadata).toHaveBeenCalledWith({ connectionId: "c1", database: "app", table: "users" });
|
||||
expect(messages[2]).toMatchObject({ id: "metadata", result: expect.objectContaining({ columns: expect.any(Array) }) });
|
||||
});
|
||||
|
||||
it("rejects malformed table metadata contexts and advertises the adapter capability", async () => {
|
||||
const messages: any[] = [];
|
||||
const target = { postMessage: (message: unknown) => messages.push(message) } as unknown as Window;
|
||||
const getTableMetadata = vi.fn();
|
||||
const bridge = new PluginHostBridge(plugin(["host.schema:read"]), workbench, {}, () => target, {
|
||||
invoke: vi.fn(),
|
||||
notify: vi.fn(),
|
||||
sendBinary: vi.fn(),
|
||||
readAsset: vi.fn(),
|
||||
getTableMetadata,
|
||||
});
|
||||
bridge.sendInit();
|
||||
expect(messages[0].capabilities).toMatchObject({ schemaMetadataApi: true });
|
||||
|
||||
const cases: [string, unknown, string][] = [
|
||||
["missing-connection", { table: "users" }, "connectionId"],
|
||||
["blank-connection", { connectionId: " ", table: "users" }, "connectionId"],
|
||||
["missing-table", { connectionId: "c1" }, "table"],
|
||||
["table-type", { connectionId: "c1", table: 7 }, "table"],
|
||||
["identifier-long", { connectionId: "c1", table: "x".repeat(257) }, "table"],
|
||||
["scope-type", { connectionId: "c1", table: "users", schema: 1 }, "schema"],
|
||||
];
|
||||
for (const [id, params] of cases) planCall(bridge, target, "host.getTableMetadata", params, id);
|
||||
planCall(bridge, target, "host.getTableMetadata", "not an object", "params-object");
|
||||
await vi.waitFor(() => expect(messages).toHaveLength(cases.length + 2));
|
||||
|
||||
cases.forEach(([id, , expected], index) => {
|
||||
expect(messages[index + 1]).toMatchObject({ id });
|
||||
expect(messages[index + 1].error).toContain(expected);
|
||||
});
|
||||
expect(messages[messages.length - 1]).toMatchObject({ id: "params-object", error: "host.getTableMetadata params must be an object" });
|
||||
expect(getTableMetadata).not.toHaveBeenCalled();
|
||||
|
||||
const legacyMessages: any[] = [];
|
||||
const legacyTarget = { postMessage: (message: unknown) => legacyMessages.push(message) } as unknown as Window;
|
||||
const legacy = new PluginHostBridge(plugin(["host.schema:read"]), workbench, {}, () => legacyTarget, {
|
||||
invoke: vi.fn(),
|
||||
notify: vi.fn(),
|
||||
sendBinary: vi.fn(),
|
||||
readAsset: vi.fn(),
|
||||
});
|
||||
legacy.sendInit();
|
||||
expect(legacyMessages[0].capabilities).toMatchObject({ schemaMetadataApi: false });
|
||||
planCall(legacy, legacyTarget, "host.getTableMetadata", { connectionId: "c1", table: "users" }, "legacy");
|
||||
await vi.waitFor(() => expect(legacyMessages).toHaveLength(2));
|
||||
expect(legacyMessages[1]).toMatchObject({ id: "legacy", error: "Host schema metadata API is unavailable" });
|
||||
});
|
||||
|
||||
it.each(["Connection session is not open for the requested database", "Query canceled", "metadata provider denied", "DBX metadata pool is busy; please retry"])("forwards schema metadata failures without retrying: %s", async (error) => {
|
||||
const messages: unknown[] = [];
|
||||
const target = { postMessage: (message: unknown) => messages.push(message) } as unknown as Window;
|
||||
const getTableMetadata = vi.fn().mockRejectedValue(new Error(error));
|
||||
const bridge = new PluginHostBridge(plugin(["host.schema:read"]), workbench, {}, () => target, {
|
||||
invoke: vi.fn(),
|
||||
notify: vi.fn(),
|
||||
sendBinary: vi.fn(),
|
||||
readAsset: vi.fn(),
|
||||
getTableMetadata,
|
||||
});
|
||||
planCall(bridge, target, "host.getTableMetadata", { connectionId: "c1", table: "users" }, "failure");
|
||||
await vi.waitFor(() => expect(messages).toHaveLength(1));
|
||||
expect(messages[0]).toMatchObject({ id: "failure", error });
|
||||
expect(getTableMetadata).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("opens only the owning plugin filesystem with explicit permission", async () => {
|
||||
const messages: unknown[] = [];
|
||||
const target = { postMessage: (message: unknown) => messages.push(message) } as unknown as Window;
|
||||
@@ -1036,6 +1125,7 @@ describe("plugin SDK source", () => {
|
||||
invoke: (method: string, params?: unknown, options?: { timeoutMs?: number }) => Promise<unknown>;
|
||||
getPlanCapabilities: (connectionId: string) => Promise<unknown>;
|
||||
explainPlan: (request: unknown) => Promise<unknown>;
|
||||
getTableMetadata: (context: unknown) => Promise<unknown>;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1123,6 +1213,17 @@ describe("plugin SDK source", () => {
|
||||
expect(requests[1].params).toEqual({ connectionId: "c1", sql: "SELECT 1", mode: "estimated" });
|
||||
});
|
||||
|
||||
it("exposes read-only table metadata through the SDK request surface", () => {
|
||||
const posted: unknown[] = [];
|
||||
const { dbxPlugin } = loadSdk(posted);
|
||||
|
||||
void dbxPlugin!.getTableMetadata({ connectionId: "c1", database: "app", table: "users" });
|
||||
|
||||
const request = firstRequest(posted);
|
||||
expect(request.method).toBe("host.getTableMetadata");
|
||||
expect(request.params).toEqual({ connectionId: "c1", database: "app", table: "users" });
|
||||
});
|
||||
|
||||
it("forwards fileTransfer requests to the host api", async () => {
|
||||
const messages: unknown[] = [];
|
||||
const target = { postMessage: (message: unknown) => messages.push(message) } as unknown as Window;
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { InstalledPlugin, PluginBinaryEvent, PluginEvent, PluginUiAssetPayl
|
||||
import { clonePluginData, snapshotPluginWorkbenchContext } from "./pluginData";
|
||||
import { MAX_PLUGIN_PLAN_SQL_CHARS, MAX_PLUGIN_PLAN_TIMEOUT_MS, PLUGIN_PLAN_PERMISSION, type PluginPlanCapabilities, type PluginPlanRequest, type PluginPlanResult } from "@/types/pluginPlan";
|
||||
import { createPluginAiConversation, type AiPluginConversationRequest } from "@/lib/ai/aiPluginConversation";
|
||||
import { MAX_PLUGIN_SCHEMA_METADATA_NAME_CHARS, PLUGIN_SCHEMA_METADATA_CAPABILITY, PLUGIN_SCHEMA_METADATA_PERMISSION, type PluginTableContext, type PluginTableMetadata } from "@/types/pluginSchemaMetadata";
|
||||
|
||||
const PLUGIN_MESSAGE_SOURCE = "dbx-plugin";
|
||||
const HOST_MESSAGE_SOURCE = "dbx-host";
|
||||
@@ -109,6 +110,8 @@ export interface PluginHostBridgeApi {
|
||||
* generates and owns the EXPLAIN statement; the plugin cannot pass one.
|
||||
*/
|
||||
explainPlan?(request: PluginPlanRequest): Promise<PluginPlanResult>;
|
||||
/** Read-only table schema metadata over an already-open Host connection. */
|
||||
getTableMetadata?(context: PluginTableContext): Promise<PluginTableMetadata>;
|
||||
closeTab?(): Promise<void> | void;
|
||||
/** Persist plugin bytes through the host's native save dialog. Resolves null when the user cancels. */
|
||||
saveFile?(pluginId: string, request: PluginSaveFileRequest, data: Uint8Array): Promise<PluginSaveFileResult | null>;
|
||||
@@ -249,6 +252,7 @@ export class PluginHostBridge {
|
||||
capabilities: {
|
||||
downloadFile: !!this.api.downloadFile,
|
||||
planApi: !!this.api.getPlanCapabilities && !!this.api.explainPlan,
|
||||
[PLUGIN_SCHEMA_METADATA_CAPABILITY]: !!this.api.getTableMetadata,
|
||||
storage: !!this.api.storageGet && !!this.api.storageSet && !!this.api.storageDelete,
|
||||
ai: !!this.api.openAiConversation,
|
||||
},
|
||||
@@ -399,6 +403,11 @@ export class PluginHostBridge {
|
||||
if (!this.api.explainPlan) throw new Error("Host plan API is unavailable");
|
||||
return this.api.explainPlan(requirePluginPlanRequest(requireRecord(params, "host.explainPlan params")));
|
||||
}
|
||||
if (method === "host.getTableMetadata") {
|
||||
this.requirePermission(PLUGIN_SCHEMA_METADATA_PERMISSION);
|
||||
if (!this.api.getTableMetadata) throw new Error("Host schema metadata API is unavailable");
|
||||
return this.api.getTableMetadata(requirePluginTableContext(requireRecord(params, "host.getTableMetadata params")));
|
||||
}
|
||||
if (method === "host.saveFile") {
|
||||
const input = isRecord(params) ? params : {};
|
||||
// The sandboxed iframe cannot trigger downloads (WKWebView cancels blob
|
||||
@@ -817,6 +826,8 @@ export function pluginSdkSource(initialTheme?: PluginBridgeTheme): string {
|
||||
// its intent, and the host refuses anything other than "estimated".
|
||||
getPlanCapabilities: (connectionId) => request('host.getPlanCapabilities', { connectionId }),
|
||||
explainPlan: (planRequest) => request('host.explainPlan', planRequest),
|
||||
// Read-only metadata; the host enforces the permission and open-session gate.
|
||||
getTableMetadata: (tableContext) => request('host.getTableMetadata', tableContext),
|
||||
saveFile: (options = {}, data) => {
|
||||
if (data === undefined) return request('host.saveFile', options);
|
||||
if (typeof data === 'string') return request('host.saveFile', { ...(options || {}), dataBase64: data });
|
||||
@@ -947,6 +958,34 @@ function optionalPluginPlanScope(value: unknown, label: string): string | undefi
|
||||
return value.trim() ? requirePluginPlanIdentifier(value, label) : undefined;
|
||||
}
|
||||
|
||||
function requirePluginTableContext(input: Record<string, unknown>): PluginTableContext {
|
||||
const context: PluginTableContext = {
|
||||
connectionId: requirePluginSchemaMetadataIdentifier(input.connectionId, "connectionId"),
|
||||
table: requirePluginSchemaMetadataIdentifier(input.table, "table"),
|
||||
};
|
||||
const database = optionalPluginSchemaMetadataIdentifier(input.database, "database");
|
||||
const schema = optionalPluginSchemaMetadataIdentifier(input.schema, "schema");
|
||||
if (database !== undefined) context.database = database;
|
||||
if (schema !== undefined) context.schema = schema;
|
||||
return context;
|
||||
}
|
||||
|
||||
function requirePluginSchemaMetadataIdentifier(value: unknown, label: string): string {
|
||||
if (typeof value !== "string") throw new Error(`${label} must be a string`);
|
||||
const identifier = value.trim();
|
||||
if (!identifier) throw new Error(`${label} must not be empty`);
|
||||
if (Array.from(identifier).length > MAX_PLUGIN_SCHEMA_METADATA_NAME_CHARS) {
|
||||
throw new Error(`${label} must be at most ${MAX_PLUGIN_SCHEMA_METADATA_NAME_CHARS} characters`);
|
||||
}
|
||||
return identifier;
|
||||
}
|
||||
|
||||
function optionalPluginSchemaMetadataIdentifier(value: unknown, label: string): string | undefined {
|
||||
if (value === undefined || value === null) return undefined;
|
||||
if (typeof value !== "string") throw new Error(`${label} must be a string`);
|
||||
return value.trim() ? requirePluginSchemaMetadataIdentifier(value, label) : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates one `host.explainPlan` request. The host owns the EXPLAIN text, so
|
||||
* the only accepted shape is the caller's own SQL plus a connection reference
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Read-only Plugin Host schema metadata contract (#9917).
|
||||
*
|
||||
* The request reuses the canonical `PluginTableContext` from the table tree;
|
||||
* this file only defines the narrow response returned by the host. It does not
|
||||
* expose DBX's internal `ColumnInfo`, comments, keys, credentials or SQL.
|
||||
*/
|
||||
|
||||
import type { PluginTableContext } from "@/types/database";
|
||||
|
||||
export type { PluginTableContext } from "@/types/database";
|
||||
|
||||
/** Permission required by `host.getTableMetadata`. */
|
||||
export const PLUGIN_SCHEMA_METADATA_PERMISSION = "host.schema:read";
|
||||
|
||||
/** Capability advertised by the bridge when the backend adapter is present. */
|
||||
export const PLUGIN_SCHEMA_METADATA_CAPABILITY = "schemaMetadataApi";
|
||||
|
||||
/** Mirrors the Rust-side identifier bound for the plugin metadata request. */
|
||||
export const MAX_PLUGIN_SCHEMA_METADATA_NAME_CHARS = 256;
|
||||
|
||||
export type PluginMetadataFieldAvailability = "supported" | "unsupported" | "unknown";
|
||||
|
||||
export interface PluginTableMetadata {
|
||||
columns: PluginColumnMetadata[];
|
||||
/** Conservative provider provenance for optional structured fields. */
|
||||
fieldCapabilities: PluginMetadataFieldCapabilities;
|
||||
}
|
||||
|
||||
export interface PluginColumnMetadata {
|
||||
name: string;
|
||||
dataType: string;
|
||||
nullable: boolean;
|
||||
length?: number | null;
|
||||
precision?: number | null;
|
||||
scale?: number | null;
|
||||
default?: string | null;
|
||||
}
|
||||
|
||||
export interface PluginMetadataFieldCapabilities {
|
||||
length: PluginMetadataFieldAvailability;
|
||||
precision: PluginMetadataFieldAvailability;
|
||||
scale: PluginMetadataFieldAvailability;
|
||||
default: PluginMetadataFieldAvailability;
|
||||
}
|
||||
|
||||
export type PluginTableMetadataRequest = PluginTableContext;
|
||||
Reference in new issue
Block a user