feat(diagram): support multi-schema and full database relationship views
This commit is contained in:
1 parent
d16645121d
commit
6d39eccdbf
18 files changed
+844
-173
No files matched your search
Vendored
+191
-1
@@ -1,6 +1,7 @@
|
||||
import { strict as assert } from "node:assert";
|
||||
import { test } from "vitest";
|
||||
import { buildDiagramJoinSql, buildDiagramRelationships, filterDiagramTables, layoutDiagramTables, normalizeCustomDiagramRelationship } from "../../apps/desktop/src/lib/diagram/erDiagram.ts";
|
||||
import { buildDiagramJoinSql, buildDiagramRelationships, diagramTableId, filterDiagramTables, layoutDiagramTables, normalizeCustomDiagramRelationship } from "../../apps/desktop/src/lib/diagram/erDiagram.ts";
|
||||
import { isTableCanvasVisible } from "../../apps/desktop/src/lib/diagram/vue-flow-adapter.ts";
|
||||
|
||||
test("builds relationships only between tables in the diagram", () => {
|
||||
const relationships = buildDiagramRelationships([
|
||||
@@ -390,3 +391,192 @@ test("E7: custom relationship cardinalities are preserved", () => {
|
||||
assert.equal(relationships[0].sourceCardinality, "N");
|
||||
assert.equal(relationships[0].targetCardinality, "N");
|
||||
});
|
||||
|
||||
test("diagramTableId formats identifier based on isMultiSchema flag", () => {
|
||||
const table = { name: "users", schema: "iam" };
|
||||
assert.equal(diagramTableId(table, false), "users");
|
||||
assert.equal(diagramTableId(table, true), "iam.users");
|
||||
assert.equal(diagramTableId({ name: "orders" }, true), "orders");
|
||||
});
|
||||
|
||||
test("buildDiagramRelationships resolves cross-schema foreign keys in multi-schema mode", () => {
|
||||
const iamUserRoles = {
|
||||
name: "user_roles",
|
||||
schema: "iam",
|
||||
columns: [
|
||||
{ name: "id", data_type: "uuid", is_nullable: false, column_default: null, is_primary_key: true, extra: null },
|
||||
{ name: "user_id", data_type: "uuid", is_nullable: false, column_default: null, is_primary_key: false, extra: null },
|
||||
],
|
||||
foreignKeys: [
|
||||
{
|
||||
name: "fk_iam_user_roles_user_id",
|
||||
column: "user_id",
|
||||
ref_schema: "public",
|
||||
ref_table: "users",
|
||||
ref_column: "id",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const publicUsers = {
|
||||
name: "users",
|
||||
schema: "public",
|
||||
columns: [
|
||||
{ name: "id", data_type: "uuid", is_nullable: false, column_default: null, is_primary_key: true, extra: null },
|
||||
],
|
||||
foreignKeys: [],
|
||||
};
|
||||
|
||||
const rels = buildDiagramRelationships([iamUserRoles, publicUsers], [], true);
|
||||
assert.equal(rels.length, 1);
|
||||
assert.equal(rels[0].sourceTable, "iam.user_roles");
|
||||
assert.equal(rels[0].sourceColumn, "user_id");
|
||||
assert.equal(rels[0].targetTable, "public.users");
|
||||
assert.equal(rels[0].targetColumn, "id");
|
||||
});
|
||||
|
||||
test("buildDiagramRelationships normalizes custom relationship tables to composite ids in multi-schema mode", () => {
|
||||
const publicUsers = {
|
||||
name: "users",
|
||||
schema: "public",
|
||||
columns: [
|
||||
{ name: "id", data_type: "uuid", is_nullable: false, column_default: null, is_primary_key: true, extra: null },
|
||||
],
|
||||
foreignKeys: [],
|
||||
};
|
||||
|
||||
const auditLogs = {
|
||||
name: "audit_log",
|
||||
schema: "audit",
|
||||
columns: [
|
||||
{ name: "actor_id", data_type: "uuid", is_nullable: false, column_default: null, is_primary_key: true, extra: null },
|
||||
],
|
||||
foreignKeys: [],
|
||||
};
|
||||
|
||||
// mixed references: match panel stores composite ids, promoted inferred relationships store plain names
|
||||
const custom = normalizeCustomDiagramRelationship({
|
||||
name: "user_audit",
|
||||
sourceTable: "public.users",
|
||||
sourceColumn: "id",
|
||||
targetTable: "audit_log",
|
||||
targetColumn: "actor_id",
|
||||
sourceCardinality: "1",
|
||||
targetCardinality: "N",
|
||||
});
|
||||
|
||||
const rels = buildDiagramRelationships([publicUsers, auditLogs], [custom], true);
|
||||
assert.equal(rels.length, 1);
|
||||
assert.equal(rels[0].kind, "custom");
|
||||
assert.equal(rels[0].sourceTable, "public.users");
|
||||
assert.equal(rels[0].targetTable, "audit.audit_log");
|
||||
assert.equal(rels[0].sourceColumn, "id");
|
||||
assert.equal(rels[0].targetColumn, "actor_id");
|
||||
});
|
||||
|
||||
test("filterDiagramTables matches schema name in search query", () => {
|
||||
const tables = [
|
||||
{ name: "logs", schema: "audit", columns: [], foreignKeys: [] },
|
||||
{ name: "users", schema: "iam", columns: [], foreignKeys: [] },
|
||||
{ name: "products", schema: "public", columns: [], foreignKeys: [] },
|
||||
];
|
||||
|
||||
const auditMatch = filterDiagramTables(tables, "audit");
|
||||
assert.equal(auditMatch.length, 1);
|
||||
assert.equal(auditMatch[0].name, "logs");
|
||||
|
||||
const qualifiedMatch = filterDiagramTables(tables, "iam.users");
|
||||
assert.equal(qualifiedMatch.length, 1);
|
||||
assert.equal(qualifiedMatch[0].name, "users");
|
||||
});
|
||||
|
||||
test("layoutDiagramTables populates schema-qualified keys in multi-schema mode", () => {
|
||||
const tables = [
|
||||
{ name: "logs", schema: "audit", columns: [] },
|
||||
{ name: "users", schema: "iam", columns: [] },
|
||||
];
|
||||
|
||||
const positions = layoutDiagramTables(tables, { columnsPerRow: 2 }, true);
|
||||
assert.ok(positions["audit.logs"]);
|
||||
assert.ok(positions["logs"]);
|
||||
assert.ok(positions["iam.users"]);
|
||||
assert.ok(positions["users"]);
|
||||
assert.deepEqual(positions["audit.logs"], positions["logs"]);
|
||||
});
|
||||
|
||||
test("buildDiagramRelationships applies strict schema-scoped foreign key resolution", () => {
|
||||
const iamUserRoles = {
|
||||
name: "user_roles",
|
||||
schema: "iam",
|
||||
columns: [
|
||||
{ name: "id", data_type: "uuid", is_nullable: false, column_default: null, is_primary_key: true, extra: null },
|
||||
{ name: "role_id", data_type: "uuid", is_nullable: false, column_default: null, is_primary_key: false, extra: null },
|
||||
],
|
||||
foreignKeys: [
|
||||
{
|
||||
name: "fk_user_roles_role_id",
|
||||
column: "role_id",
|
||||
// no ref_schema: should strictly look in "iam"
|
||||
ref_table: "roles",
|
||||
ref_column: "id",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const publicRoles = {
|
||||
name: "roles",
|
||||
schema: "public",
|
||||
columns: [
|
||||
{ name: "id", data_type: "uuid", is_nullable: false, column_default: null, is_primary_key: true, extra: null },
|
||||
],
|
||||
foreignKeys: [],
|
||||
};
|
||||
|
||||
const iamRoles = {
|
||||
name: "roles",
|
||||
schema: "iam",
|
||||
columns: [
|
||||
{ name: "id", data_type: "uuid", is_nullable: false, column_default: null, is_primary_key: true, extra: null },
|
||||
],
|
||||
foreignKeys: [],
|
||||
};
|
||||
|
||||
// In multi-schema mode: without ref_schema, matches iam.roles, never public.roles
|
||||
const relsWithIam = buildDiagramRelationships([iamUserRoles, publicRoles, iamRoles], [], true);
|
||||
assert.equal(relsWithIam.length, 1);
|
||||
assert.equal(relsWithIam[0].sourceTable, "iam.user_roles");
|
||||
assert.equal(relsWithIam[0].targetTable, "iam.roles");
|
||||
|
||||
// In multi-schema mode: if iam.roles does NOT exist, do NOT fall back across schemas
|
||||
const relsWithoutIam = buildDiagramRelationships([iamUserRoles, publicRoles], [], true);
|
||||
assert.equal(relsWithoutIam.length, 0);
|
||||
|
||||
// In single-schema mode: falls back to matching table by bare name
|
||||
const singleSchemaUserRoles = { ...iamUserRoles, schema: undefined };
|
||||
const singleSchemaRoles = { ...publicRoles, schema: undefined };
|
||||
const singleSchemaRels = buildDiagramRelationships([singleSchemaUserRoles, singleSchemaRoles], [], false);
|
||||
assert.equal(singleSchemaRels.length, 1);
|
||||
assert.equal(singleSchemaRels[0].sourceTable, "user_roles");
|
||||
assert.equal(singleSchemaRels[0].targetTable, "roles");
|
||||
});
|
||||
|
||||
test("isTableCanvasVisible respects schema qualification and raw fallback when unambiguous", () => {
|
||||
const layer = {
|
||||
id: "layer-1",
|
||||
name: "Layer 1",
|
||||
color: "#ff0000",
|
||||
tableNames: ["iam.users", "legacy_orders"],
|
||||
collapsed: false,
|
||||
visible: false,
|
||||
};
|
||||
|
||||
// iam.users is hidden by the layer
|
||||
assert.equal(isTableCanvasVisible("iam.users", [layer], "users"), false);
|
||||
|
||||
// public.users is NOT in layer-1, and since rawName "users" is NOT in layer.tableNames, it stays visible
|
||||
assert.equal(isTableCanvasVisible("public.users", [layer], undefined), true);
|
||||
|
||||
// legacy_orders using bare name fallback when unambiguous
|
||||
assert.equal(isTableCanvasVisible("public.legacy_orders", [layer], "legacy_orders"), false);
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user