feat(xugu): expose programmable object metadata
This commit is contained in:
1 parent
d3887c9936
commit
08cd8903f4
36 files changed
+431
-62
No files matched your search
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { doubleClickRowAction, resolveRowClickAction, shouldDeferSingleClick, singleClickRowAction } from "@/lib/table/objectBrowserRowAction";
|
||||
import { doubleClickRowAction, isSourceOnlyObjectBrowserRow, resolveRowClickAction, shouldDeferSingleClick, singleClickRowAction } from "@/lib/table/objectBrowserRowAction";
|
||||
import type { ObjectBrowserRow } from "@/lib/table/objectBrowserRows";
|
||||
|
||||
function row(type: ObjectBrowserRow["type"], name = "test"): ObjectBrowserRow {
|
||||
@@ -39,6 +39,10 @@ describe("singleClickRowAction", () => {
|
||||
expect(singleClickRowAction(row("PACKAGE_BODY", "pkg_body_test"))).toBe("open-source");
|
||||
});
|
||||
|
||||
it.each(["TRIGGER", "TYPE", "TYPE_BODY"] as const)("returns open-source for %s", (type) => {
|
||||
expect(singleClickRowAction(row(type, "programmable_test"))).toBe("open-source");
|
||||
});
|
||||
|
||||
it("returns none for null/undefined", () => {
|
||||
expect(singleClickRowAction(null)).toBe("none");
|
||||
expect(singleClickRowAction(undefined)).toBe("none");
|
||||
@@ -63,6 +67,17 @@ describe("doubleClickRowAction", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("isSourceOnlyObjectBrowserRow", () => {
|
||||
it.each(["TRIGGER", "TYPE", "TYPE_BODY"] as const)("marks %s as source-only", (type) => {
|
||||
expect(isSourceOnlyObjectBrowserRow(row(type))).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps procedure and function mutation menus separate", () => {
|
||||
expect(isSourceOnlyObjectBrowserRow(row("PROCEDURE"))).toBe(false);
|
||||
expect(isSourceOnlyObjectBrowserRow(row("FUNCTION"))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveRowClickAction", () => {
|
||||
const tableRow = row("TABLE", "users");
|
||||
const viewRow = row("VIEW", "v_users");
|
||||
|
||||
@@ -29,6 +29,22 @@ describe("PostgreSQL overloaded routines", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("programmable database objects", () => {
|
||||
it("keeps Xugu trigger/type nodes distinct and preserves an invalid status", () => {
|
||||
const objects: ObjectInfo[] = [
|
||||
{ name: "TRG_AUDIT", object_type: "TRIGGER", schema: "APP", valid: false },
|
||||
{ name: "ADDRESS_T", object_type: "TYPE", schema: "APP", valid: true },
|
||||
{ name: "ADDRESS_T", object_type: "TYPE_BODY", schema: "APP", valid: true },
|
||||
];
|
||||
|
||||
const nodes = buildSimpleObjectTreeNodes({ ...context, schema: "APP", objects });
|
||||
|
||||
expect(nodes).toEqual(
|
||||
expect.arrayContaining([expect.objectContaining({ type: "trigger", objectName: "TRG_AUDIT", valid: false }), expect.objectContaining({ type: "type", objectName: "ADDRESS_T", valid: true }), expect.objectContaining({ type: "type-body", objectName: "ADDRESS_T", valid: true })]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("PostgreSQL table hierarchy", () => {
|
||||
it("keeps schema pagination visible at the table-group root when a page ends inside nested partitions", () => {
|
||||
const nodes = buildTableTreeNodes({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { DatabaseType } from "@/types/database";
|
||||
|
||||
export type SidebarObjectKind = "TABLE" | "VIEW" | "MATERIALIZED_VIEW" | "PROCEDURE" | "FUNCTION" | "SEQUENCE" | "PACKAGE" | "PACKAGE_BODY";
|
||||
export type SidebarObjectKind = "TABLE" | "VIEW" | "MATERIALIZED_VIEW" | "PROCEDURE" | "FUNCTION" | "TRIGGER" | "SEQUENCE" | "PACKAGE" | "PACKAGE_BODY" | "TYPE" | "TYPE_BODY";
|
||||
|
||||
export interface DatabaseObjectCapabilities {
|
||||
sidebarObjects: SidebarObjectKind[];
|
||||
@@ -15,6 +15,7 @@ const ROUTINE_OBJECTS: SidebarObjectKind[] = ["TABLE", "VIEW", "PROCEDURE", "FUN
|
||||
const POSTGRES_OBJECTS: SidebarObjectKind[] = ["TABLE", "VIEW", "MATERIALIZED_VIEW", "PROCEDURE", "FUNCTION", "SEQUENCE"];
|
||||
const POSTGRES_LIKE_OBJECTS: SidebarObjectKind[] = ["TABLE", "VIEW", "MATERIALIZED_VIEW", "PROCEDURE", "FUNCTION"];
|
||||
const ORACLE_OBJECTS: SidebarObjectKind[] = ["TABLE", "VIEW", "MATERIALIZED_VIEW", "PROCEDURE", "FUNCTION", "PACKAGE", "PACKAGE_BODY"];
|
||||
const XUGU_OBJECTS: SidebarObjectKind[] = ["TABLE", "VIEW", "PROCEDURE", "FUNCTION", "TRIGGER", "SEQUENCE", "PACKAGE", "PACKAGE_BODY", "TYPE", "TYPE_BODY"];
|
||||
|
||||
const DATABASE_TYPE_OBJECTS = new Map<DatabaseType, SidebarObjectKind[]>([
|
||||
// postgres
|
||||
@@ -31,6 +32,7 @@ const DATABASE_TYPE_OBJECTS = new Map<DatabaseType, SidebarObjectKind[]>([
|
||||
["oracle", ORACLE_OBJECTS],
|
||||
["dameng", ORACLE_OBJECTS],
|
||||
["oceanbase-oracle", ORACLE_OBJECTS],
|
||||
["xugu", XUGU_OBJECTS],
|
||||
// table and view
|
||||
["sqlite", TABLE_VIEW_OBJECTS],
|
||||
["rqlite", TABLE_VIEW_OBJECTS],
|
||||
@@ -74,7 +76,10 @@ export function normalizeSidebarObjectKind(type: string): SidebarObjectKind {
|
||||
const value = type.toUpperCase();
|
||||
const normalized = value.replace(/[\s-]+/g, "_");
|
||||
if (normalized.includes("PACKAGE_BODY")) return "PACKAGE_BODY";
|
||||
if (normalized.includes("TYPE_BODY")) return "TYPE_BODY";
|
||||
if (normalized.includes("PACKAGE")) return "PACKAGE";
|
||||
if (normalized.includes("TRIGGER")) return "TRIGGER";
|
||||
if (normalized.includes("TYPE")) return "TYPE";
|
||||
if (normalized.includes("MATERIALIZED_VIEW")) return "MATERIALIZED_VIEW";
|
||||
if (value.includes("VIEW")) return "VIEW";
|
||||
if (value.includes("SEQ")) return "SEQUENCE";
|
||||
|
||||
@@ -9,6 +9,8 @@ const leafTypes: Set<TreeNodeType> = new Set([
|
||||
"function",
|
||||
"package",
|
||||
"package-body",
|
||||
"type",
|
||||
"type-body",
|
||||
"object-browser",
|
||||
"redis-db",
|
||||
"mq-tenant",
|
||||
|
||||
@@ -10,19 +10,22 @@ const dataNodeTypes = new Set<TreeNodeType>(["table", "view", "materialized_view
|
||||
const documentBrowserNodeTypes = new Set<TreeNodeType>(["mongo-collection", "mongo-bucket"]);
|
||||
const toggleLeafNodeTypes = new Set<TreeNodeType>(["redis-db", "mq-tenant", "etcd-root", "zookeeper-root", "mongo-gridfs", "mongo-collection", "mongo-bucket", "vector-collection", "elasticsearch-index", "user-admin"]);
|
||||
const objectBrowserNodeTypes = new Set<TreeNodeType>(["database", "schema", "object-browser"]);
|
||||
const sourceNodeTypes = new Set<TreeNodeType>(["materialized_view", "procedure", "function", "sequence", "package", "package-body"]);
|
||||
const sourceNodeTypes = new Set<TreeNodeType>(["materialized_view", "procedure", "function", "trigger", "sequence", "package", "package-body", "type", "type-body"]);
|
||||
const savedSqlNodeTypes = new Set<TreeNodeType>(["saved-sql-file"]);
|
||||
const tableChildGroupNodeTypes = new Set<TreeNodeType>(["group-columns", "group-indexes", "group-fkeys", "group-triggers", "group-partitions"]);
|
||||
const databaseChildGroupNodeTypes = new Set<TreeNodeType>(["group-tables", "group-views", "group-materialized-views", "group-procedures", "group-functions", "group-sequences", "group-packages"]);
|
||||
const databaseChildGroupNodeTypes = new Set<TreeNodeType>(["group-tables", "group-views", "group-materialized-views", "group-procedures", "group-functions", "group-triggers", "group-sequences", "group-packages", "group-types"]);
|
||||
|
||||
export function objectSourceKindForTreeNode(type: TreeNodeType): ObjectSourceKind | null {
|
||||
if (type === "view") return "VIEW";
|
||||
if (type === "materialized_view") return "MATERIALIZED_VIEW";
|
||||
if (type === "procedure") return "PROCEDURE";
|
||||
if (type === "function") return "FUNCTION";
|
||||
if (type === "trigger") return "TRIGGER";
|
||||
if (type === "sequence") return "SEQUENCE";
|
||||
if (type === "package") return "PACKAGE";
|
||||
if (type === "package-body") return "PACKAGE_BODY";
|
||||
if (type === "type") return "TYPE";
|
||||
if (type === "type-body") return "TYPE_BODY";
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ export type ObjectBrowserRowAction = "table-info" | "open-table" | "open-source"
|
||||
/**
|
||||
* Determine the action for a single click on an object browser row.
|
||||
* - TABLE → table-info (show table properties panel)
|
||||
* - VIEW/MATERIALIZED_VIEW/PROCEDURE/FUNCTION/SEQUENCE/PACKAGE/PACKAGE_BODY → open-source
|
||||
* - VIEW/MATERIALIZED_VIEW/PROCEDURE/FUNCTION/TRIGGER/SEQUENCE/PACKAGE/PACKAGE_BODY/TYPE/TYPE_BODY → open-source
|
||||
* - otherwise → none
|
||||
*/
|
||||
export function singleClickRowAction(row: ObjectBrowserRow | null | undefined): ObjectBrowserRowAction {
|
||||
@@ -18,7 +18,7 @@ export function singleClickRowAction(row: ObjectBrowserRow | null | undefined):
|
||||
/**
|
||||
* Determine the action for a double click on an object browser row.
|
||||
* - TABLE → open-table (open table data tab)
|
||||
* - VIEW/MATERIALIZED_VIEW/PROCEDURE/FUNCTION/SEQUENCE/PACKAGE/PACKAGE_BODY → open-source
|
||||
* - VIEW/MATERIALIZED_VIEW/PROCEDURE/FUNCTION/TRIGGER/SEQUENCE/PACKAGE/PACKAGE_BODY/TYPE/TYPE_BODY → open-source
|
||||
* - otherwise → none
|
||||
*/
|
||||
export function doubleClickRowAction(row: ObjectBrowserRow | null | undefined): ObjectBrowserRowAction {
|
||||
@@ -62,6 +62,14 @@ export function shouldDeferSingleClick(row: ObjectBrowserRow | null | undefined,
|
||||
return single !== double && action === single;
|
||||
}
|
||||
|
||||
/**
|
||||
* Objects with source metadata but no supported object-browser mutation API.
|
||||
* Their menu intentionally exposes only source viewing and copying.
|
||||
*/
|
||||
export function isSourceOnlyObjectBrowserRow(row: ObjectBrowserRow): boolean {
|
||||
return row.type === "TRIGGER" || row.type === "SEQUENCE" || row.type === "PACKAGE" || row.type === "PACKAGE_BODY" || row.type === "TYPE" || row.type === "TYPE_BODY";
|
||||
}
|
||||
|
||||
function canOpenSource(row: ObjectBrowserRow): boolean {
|
||||
return row.type === "VIEW" || row.type === "MATERIALIZED_VIEW" || row.type === "PROCEDURE" || row.type === "FUNCTION" || row.type === "SEQUENCE" || row.type === "PACKAGE" || row.type === "PACKAGE_BODY";
|
||||
return row.type === "VIEW" || row.type === "MATERIALIZED_VIEW" || row.type === "PROCEDURE" || row.type === "FUNCTION" || row.type === "TRIGGER" || row.type === "SEQUENCE" || row.type === "PACKAGE" || row.type === "PACKAGE_BODY" || row.type === "TYPE" || row.type === "TYPE_BODY";
|
||||
}
|
||||
@@ -7,7 +7,8 @@ export type ObjectBrowserRow = {
|
||||
name: string;
|
||||
displayName: string;
|
||||
schema?: string;
|
||||
type: "TABLE" | "VIEW" | "MATERIALIZED_VIEW" | "PROCEDURE" | "FUNCTION" | "SEQUENCE" | "PACKAGE" | "PACKAGE_BODY";
|
||||
type: "TABLE" | "VIEW" | "MATERIALIZED_VIEW" | "PROCEDURE" | "FUNCTION" | "TRIGGER" | "SEQUENCE" | "PACKAGE" | "PACKAGE_BODY" | "TYPE" | "TYPE_BODY";
|
||||
valid?: boolean | null;
|
||||
signature?: string | null;
|
||||
comment?: string | null;
|
||||
created_at?: string | null;
|
||||
@@ -27,7 +28,10 @@ export function normalizeObjectBrowserType(type: string): ObjectBrowserRow["type
|
||||
const value = type.toUpperCase();
|
||||
const normalized = value.replace(/[\s-]+/g, "_");
|
||||
if (normalized.includes("PACKAGE_BODY")) return "PACKAGE_BODY";
|
||||
if (normalized.includes("TYPE_BODY")) return "TYPE_BODY";
|
||||
if (normalized.includes("PACKAGE")) return "PACKAGE";
|
||||
if (normalized.includes("TRIGGER")) return "TRIGGER";
|
||||
if (normalized.includes("TYPE")) return "TYPE";
|
||||
if (normalized.includes("MATERIALIZED_VIEW")) return "MATERIALIZED_VIEW";
|
||||
if (value.includes("VIEW")) return "VIEW";
|
||||
if (value.includes("SEQ")) return "SEQUENCE";
|
||||
@@ -57,6 +61,7 @@ export function buildObjectBrowserRows(options: { objects: ObjectInfo[]; databas
|
||||
displayName,
|
||||
schema,
|
||||
type,
|
||||
valid: object.valid,
|
||||
signature,
|
||||
comment: object.comment,
|
||||
created_at: object.created_at,
|
||||
|
||||
@@ -504,7 +504,7 @@ export function buildSimpleObjectTreeNodes({ nodeId, connectionId, database, sch
|
||||
|
||||
for (const obj of objects) {
|
||||
const objectType = normalizeObjectType(obj.object_type);
|
||||
if (!["TABLE", "VIEW", "MATERIALIZED_VIEW", "PROCEDURE", "FUNCTION", "SEQUENCE", "PACKAGE", "PACKAGE_BODY"].includes(objectType)) {
|
||||
if (!["TABLE", "VIEW", "MATERIALIZED_VIEW", "PROCEDURE", "FUNCTION", "TRIGGER", "SEQUENCE", "PACKAGE", "PACKAGE_BODY", "TYPE", "TYPE_BODY"].includes(objectType)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -540,6 +540,7 @@ export function buildSimpleObjectTreeNodes({ nodeId, connectionId, database, sch
|
||||
objectName: name,
|
||||
signature: signature || undefined,
|
||||
comment: obj.comment,
|
||||
valid: obj.valid ?? undefined,
|
||||
connectionId,
|
||||
database,
|
||||
schema: childSchema,
|
||||
@@ -557,9 +558,12 @@ function simpleObjectNodeType(objectType: DatabaseObjectTreeKind): TreeNodeType
|
||||
if (objectType === "MATERIALIZED_VIEW") return "materialized_view";
|
||||
if (objectType === "PROCEDURE") return "procedure";
|
||||
if (objectType === "FUNCTION") return "function";
|
||||
if (objectType === "TRIGGER") return "trigger";
|
||||
if (objectType === "SEQUENCE") return "sequence";
|
||||
if (objectType === "PACKAGE_BODY") return "package-body";
|
||||
if (objectType === "PACKAGE") return "package";
|
||||
if (objectType === "TYPE_BODY") return "type-body";
|
||||
if (objectType === "TYPE") return "type";
|
||||
return "table";
|
||||
}
|
||||
|
||||
@@ -597,6 +601,13 @@ const groupDefs: Array<{
|
||||
nodeType: "group-functions",
|
||||
childType: "function",
|
||||
},
|
||||
{
|
||||
key: "__triggers",
|
||||
label: "tree.triggers",
|
||||
objectTypes: ["TRIGGER"],
|
||||
nodeType: "group-triggers",
|
||||
childType: "trigger",
|
||||
},
|
||||
{
|
||||
key: "__sequences",
|
||||
label: "tree.sequences",
|
||||
@@ -611,9 +622,16 @@ const groupDefs: Array<{
|
||||
nodeType: "group-packages",
|
||||
childType: (objectType) => (objectType === "PACKAGE_BODY" ? "package-body" : "package"),
|
||||
},
|
||||
{
|
||||
key: "__types",
|
||||
label: "tree.types",
|
||||
objectTypes: ["TYPE", "TYPE_BODY"],
|
||||
nodeType: "group-types",
|
||||
childType: (objectType) => (objectType === "TYPE_BODY" ? "type-body" : "type"),
|
||||
},
|
||||
];
|
||||
|
||||
const objectGroupNodeTypes = new Set<TreeNodeType>(["group-tables", "group-views", "group-materialized-views", "group-procedures", "group-functions", "group-sequences", "group-packages"]);
|
||||
const objectGroupNodeTypes = new Set<TreeNodeType>(["group-tables", "group-views", "group-materialized-views", "group-procedures", "group-functions", "group-triggers", "group-sequences", "group-packages", "group-types"]);
|
||||
|
||||
export function buildObjectGroupPlaceholderNodes({ nodeId, connectionId, database, schema, objectTypes }: { nodeId: string; connectionId: string; database: string; schema?: string; objectTypes: DatabaseObjectTreeKind[] }): TreeNode[] {
|
||||
const supported = new Set(objectTypes);
|
||||
@@ -676,12 +694,13 @@ export function buildGroupedObjectTreeNodes({ nodeId, connectionId, database, sc
|
||||
const childSchema = obj.schema ? normalizeDatabaseObjectName(obj.schema) : schema;
|
||||
const objectType = normalizeObjectType(obj.object_type);
|
||||
const childType = typeof def.childType === "function" ? def.childType(objectType) : def.childType;
|
||||
const objectTypeSuffix = objectType === "PACKAGE" || objectType === "PACKAGE_BODY" ? `:${objectType}` : "";
|
||||
const objectTypeSuffix = objectType === "PACKAGE" || objectType === "PACKAGE_BODY" || objectType === "TYPE" || objectType === "TYPE_BODY" ? `:${objectType}` : "";
|
||||
return {
|
||||
id: `${nodeId}:${def.key}:${childSchema ? `${childSchema}:` : ""}${obj.name}${objectTypeSuffix}`,
|
||||
label: obj.name,
|
||||
type: childType,
|
||||
comment: obj.comment,
|
||||
valid: obj.valid ?? undefined,
|
||||
connectionId,
|
||||
database,
|
||||
schema: childSchema,
|
||||
|
||||
Reference in new issue
Block a user