@rdbms-erd/core 0.1.3 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +237 -0
- package/dist/index.js +883 -0
- package/dist/index.js.map +1 -0
- package/package.json +18 -4
- package/src/__tests__/alignment.test.ts +0 -19
- package/src/__tests__/benchmark.test.ts +0 -14
- package/src/__tests__/db-meta-adapter.test.ts +0 -166
- package/src/__tests__/ddl-diagnostics.test.ts +0 -52
- package/src/__tests__/ddl-snapshot.test.ts +0 -91
- package/src/__tests__/designDocument.test.ts +0 -129
- package/src/__tests__/logical-physical.test.ts +0 -110
- package/src/alignment.ts +0 -71
- package/src/index.ts +0 -1386
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
createColumn,
|
|
4
|
-
createEmptyDesign,
|
|
5
|
-
isRelationshipLineRenderable,
|
|
6
|
-
parseDesign,
|
|
7
|
-
roundTripDesign,
|
|
8
|
-
serializeDesign,
|
|
9
|
-
validateDesignDocument,
|
|
10
|
-
} from "../index";
|
|
11
|
-
|
|
12
|
-
describe("DesignDocument", () => {
|
|
13
|
-
it("round-trips", () => {
|
|
14
|
-
const doc = createEmptyDesign("mysql");
|
|
15
|
-
doc.model.tables.push({
|
|
16
|
-
id: "t1",
|
|
17
|
-
logicalName: "엔티티",
|
|
18
|
-
physicalName: "TB_T1",
|
|
19
|
-
columns: []
|
|
20
|
-
});
|
|
21
|
-
const again = roundTripDesign(doc);
|
|
22
|
-
expect(again.model.dialect).toBe("mysql");
|
|
23
|
-
expect(again.model.tables).toHaveLength(1);
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it("rejects invalid dialect", () => {
|
|
27
|
-
const raw = JSON.parse(serializeDesign(createEmptyDesign()));
|
|
28
|
-
raw.model.dialect = "mariadb";
|
|
29
|
-
expect(() => validateDesignDocument(raw)).toThrow(/dialect/);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it("parseDesign validates", () => {
|
|
33
|
-
const json = serializeDesign(createEmptyDesign("oracle"));
|
|
34
|
-
expect(parseDesign(json).model.dialect).toBe("oracle");
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it("createEmptyDesign defaults to mssql", () => {
|
|
38
|
-
expect(createEmptyDesign().model.dialect).toBe("mssql");
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it("isRelationshipLineRenderable respects relationship canvasLineHidden and reveal toggle", () => {
|
|
42
|
-
const doc = createEmptyDesign("postgres");
|
|
43
|
-
doc.model.tables.push(
|
|
44
|
-
{
|
|
45
|
-
id: "a",
|
|
46
|
-
logicalName: "A",
|
|
47
|
-
physicalName: "TA",
|
|
48
|
-
columns: [createColumn("postgres", { id: "apk", logicalName: "id", logicalType: "NUMBER", nullable: false, isPrimaryKey: true })]
|
|
49
|
-
},
|
|
50
|
-
{
|
|
51
|
-
id: "b",
|
|
52
|
-
logicalName: "B",
|
|
53
|
-
physicalName: "TB",
|
|
54
|
-
columns: [
|
|
55
|
-
createColumn("postgres", {
|
|
56
|
-
id: "bfk",
|
|
57
|
-
logicalName: "aid",
|
|
58
|
-
logicalType: "NUMBER",
|
|
59
|
-
isForeignKey: true
|
|
60
|
-
})
|
|
61
|
-
]
|
|
62
|
-
}
|
|
63
|
-
);
|
|
64
|
-
const rel = {
|
|
65
|
-
id: "r1",
|
|
66
|
-
sourceTableId: "a",
|
|
67
|
-
targetTableId: "b",
|
|
68
|
-
sourceColumnId: "apk",
|
|
69
|
-
targetColumnId: "bfk",
|
|
70
|
-
canvasLineHidden: true as const
|
|
71
|
-
};
|
|
72
|
-
doc.model.relationships.push(rel);
|
|
73
|
-
expect(isRelationshipLineRenderable(rel, false)).toBe(false);
|
|
74
|
-
expect(isRelationshipLineRenderable(rel, true)).toBe(true);
|
|
75
|
-
delete rel.canvasLineHidden;
|
|
76
|
-
expect(isRelationshipLineRenderable(rel, false)).toBe(true);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it("migrates legacy column showFkRelationLine false to relationship canvasLineHidden", () => {
|
|
80
|
-
const doc = createEmptyDesign("postgres");
|
|
81
|
-
doc.model.tables.push(
|
|
82
|
-
{
|
|
83
|
-
id: "a",
|
|
84
|
-
logicalName: "A",
|
|
85
|
-
physicalName: "TA",
|
|
86
|
-
columns: [
|
|
87
|
-
createColumn("postgres", {
|
|
88
|
-
id: "apk",
|
|
89
|
-
logicalName: "id",
|
|
90
|
-
logicalType: "NUMBER",
|
|
91
|
-
nullable: false,
|
|
92
|
-
isPrimaryKey: true,
|
|
93
|
-
}),
|
|
94
|
-
],
|
|
95
|
-
},
|
|
96
|
-
{
|
|
97
|
-
id: "b",
|
|
98
|
-
logicalName: "B",
|
|
99
|
-
physicalName: "TB",
|
|
100
|
-
columns: [
|
|
101
|
-
createColumn("postgres", {
|
|
102
|
-
id: "bfk",
|
|
103
|
-
logicalName: "aid",
|
|
104
|
-
logicalType: "NUMBER",
|
|
105
|
-
isForeignKey: true,
|
|
106
|
-
}),
|
|
107
|
-
],
|
|
108
|
-
},
|
|
109
|
-
);
|
|
110
|
-
doc.model.relationships.push({
|
|
111
|
-
id: "r1",
|
|
112
|
-
sourceTableId: "a",
|
|
113
|
-
targetTableId: "b",
|
|
114
|
-
sourceColumnId: "apk",
|
|
115
|
-
targetColumnId: "bfk",
|
|
116
|
-
});
|
|
117
|
-
const raw = JSON.parse(serializeDesign(doc)) as Record<string, unknown>;
|
|
118
|
-
const tables = (raw.model as { tables: { id: string; columns: Record<string, unknown>[] }[] }).tables;
|
|
119
|
-
const bTable = tables.find((t) => t.id === "b");
|
|
120
|
-
const fkCol = bTable?.columns.find((c) => c.id === "bfk");
|
|
121
|
-
if (fkCol) fkCol.showFkRelationLine = false;
|
|
122
|
-
const out = validateDesignDocument(raw);
|
|
123
|
-
expect(out.model.relationships.find((r) => r.id === "r1")?.canvasLineHidden).toBe(true);
|
|
124
|
-
const migratedFk = out.model.tables.find((t) => t.id === "b")?.columns.find((c) => c.id === "bfk");
|
|
125
|
-
expect(
|
|
126
|
-
migratedFk && "showFkRelationLine" in migratedFk ? (migratedFk as { showFkRelationLine?: boolean }).showFkRelationLine : undefined,
|
|
127
|
-
).toBeUndefined();
|
|
128
|
-
});
|
|
129
|
-
});
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
applyLogicalTypeChange,
|
|
4
|
-
convertDesignDialect,
|
|
5
|
-
createColumn,
|
|
6
|
-
createEmptyDesign,
|
|
7
|
-
defaultPhysicalType,
|
|
8
|
-
inferLogicalTypeFromPhysical,
|
|
9
|
-
} from "../index";
|
|
10
|
-
|
|
11
|
-
describe("logical / physical types", () => {
|
|
12
|
-
it("createColumn defaults physical name to logical name and sets default physical type", () => {
|
|
13
|
-
const col = createColumn("postgres", {
|
|
14
|
-
id: "c1",
|
|
15
|
-
logicalName: "이름",
|
|
16
|
-
logicalType: "TEXT"
|
|
17
|
-
});
|
|
18
|
-
expect(col.physicalName).toBe("이름");
|
|
19
|
-
expect(col.physicalType).toBe(defaultPhysicalType("postgres", "TEXT"));
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it("createColumn uses explicit physical name when provided", () => {
|
|
23
|
-
const col = createColumn("postgres", {
|
|
24
|
-
id: "c1",
|
|
25
|
-
logicalName: "이름",
|
|
26
|
-
physicalName: "Name",
|
|
27
|
-
logicalType: "TEXT"
|
|
28
|
-
});
|
|
29
|
-
expect(col.physicalName).toBe("Name");
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it("applyLogicalTypeChange resets physical to default", () => {
|
|
33
|
-
let col = createColumn("mssql", {
|
|
34
|
-
id: "c1",
|
|
35
|
-
logicalName: "수량",
|
|
36
|
-
physicalName: "Qty",
|
|
37
|
-
logicalType: "NUMBER"
|
|
38
|
-
});
|
|
39
|
-
col = { ...col, physicalType: "BIGINT" };
|
|
40
|
-
col = applyLogicalTypeChange(col, "FLOAT", "mssql");
|
|
41
|
-
expect(col.logicalType).toBe("FLOAT");
|
|
42
|
-
expect(col.physicalType).toBe(defaultPhysicalType("mssql", "FLOAT"));
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it("convertDesignDialect preserves numeric precision/scale when possible", () => {
|
|
46
|
-
const doc = createEmptyDesign("mssql");
|
|
47
|
-
doc.model.tables.push({
|
|
48
|
-
id: "t1",
|
|
49
|
-
logicalName: "주문",
|
|
50
|
-
physicalName: "orders",
|
|
51
|
-
columns: [
|
|
52
|
-
{
|
|
53
|
-
...createColumn("mssql", {
|
|
54
|
-
id: "c1",
|
|
55
|
-
logicalName: "금액",
|
|
56
|
-
physicalName: "amount",
|
|
57
|
-
logicalType: "NUMBER"
|
|
58
|
-
}),
|
|
59
|
-
physicalType: "NUMERIC(10,5)"
|
|
60
|
-
}
|
|
61
|
-
]
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
const oracleDoc = convertDesignDialect(doc, "oracle");
|
|
65
|
-
expect(oracleDoc.model.tables[0].columns[0].physicalType).toBe("NUMBER(10,5)");
|
|
66
|
-
|
|
67
|
-
const pgDoc = convertDesignDialect(doc, "postgres");
|
|
68
|
-
expect(pgDoc.model.tables[0].columns[0].physicalType).toBe("NUMERIC(10,5)");
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it("convertDesignDialect preserves varchar length when possible", () => {
|
|
72
|
-
const doc = createEmptyDesign("postgres");
|
|
73
|
-
doc.model.tables.push({
|
|
74
|
-
id: "t1",
|
|
75
|
-
logicalName: "상품",
|
|
76
|
-
physicalName: "products",
|
|
77
|
-
columns: [
|
|
78
|
-
{
|
|
79
|
-
...createColumn("postgres", {
|
|
80
|
-
id: "c1",
|
|
81
|
-
logicalName: "코드",
|
|
82
|
-
physicalName: "code",
|
|
83
|
-
logicalType: "TEXT"
|
|
84
|
-
}),
|
|
85
|
-
physicalType: "VARCHAR(120)"
|
|
86
|
-
}
|
|
87
|
-
]
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
const mssqlDoc = convertDesignDialect(doc, "mssql");
|
|
91
|
-
expect(mssqlDoc.model.tables[0].columns[0].physicalType).toBe("NVARCHAR(120)");
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it("defaultPhysicalType includes newly added logical types", () => {
|
|
95
|
-
expect(defaultPhysicalType("postgres", "BOOLEAN")).toBe("BOOLEAN");
|
|
96
|
-
expect(defaultPhysicalType("mysql", "JSON")).toBe("JSON");
|
|
97
|
-
expect(defaultPhysicalType("oracle", "UUID")).toBe("RAW(16)");
|
|
98
|
-
expect(defaultPhysicalType("sqlite", "BINARY")).toBe("BLOB");
|
|
99
|
-
expect(defaultPhysicalType("mssql", "DECIMAL")).toBe("DECIMAL(10,2)");
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it("inferLogicalTypeFromPhysical maps defaults and common aliases", () => {
|
|
103
|
-
expect(inferLogicalTypeFromPhysical("mssql", "INT")).toBe("NUMBER");
|
|
104
|
-
expect(inferLogicalTypeFromPhysical("mssql", "NVARCHAR(500)")).toBe("TEXT");
|
|
105
|
-
expect(inferLogicalTypeFromPhysical("mssql", "DATETIME2")).toBe("DATETIME");
|
|
106
|
-
expect(inferLogicalTypeFromPhysical("mssql", "BIT")).toBe("BOOLEAN");
|
|
107
|
-
expect(inferLogicalTypeFromPhysical("postgres", "INTEGER")).toBe("NUMBER");
|
|
108
|
-
expect(inferLogicalTypeFromPhysical("postgres", "VARCHAR(255)")).toBe("TEXT");
|
|
109
|
-
});
|
|
110
|
-
});
|
package/src/alignment.ts
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
export type AlignCommand =
|
|
2
|
-
| "left"
|
|
3
|
-
| "h-center"
|
|
4
|
-
| "right"
|
|
5
|
-
| "top"
|
|
6
|
-
| "v-center"
|
|
7
|
-
| "bottom"
|
|
8
|
-
| "h-gap"
|
|
9
|
-
| "v-gap";
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* 멀티 선택된 노드 위치를 bounding box 기준으로 정렬/분배한다.
|
|
13
|
-
* positions는 id -> 좌표 맵이며, 반환값은 동일 키에 갱신된 좌표만 포함한다.
|
|
14
|
-
*/
|
|
15
|
-
export function alignNodePositions(
|
|
16
|
-
selectedIds: string[],
|
|
17
|
-
positions: Record<string, { x: number; y: number }>,
|
|
18
|
-
command: AlignCommand
|
|
19
|
-
): Record<string, { x: number; y: number }> {
|
|
20
|
-
const entries = selectedIds
|
|
21
|
-
.map((id) => {
|
|
22
|
-
const pos = positions[id];
|
|
23
|
-
return pos ? { id, pos: { ...pos } } : null;
|
|
24
|
-
})
|
|
25
|
-
.filter((e): e is { id: string; pos: { x: number; y: number } } => Boolean(e));
|
|
26
|
-
|
|
27
|
-
if (entries.length < 2) {
|
|
28
|
-
return {};
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const xs = entries.map((e) => e.pos.x);
|
|
32
|
-
const ys = entries.map((e) => e.pos.y);
|
|
33
|
-
const minX = Math.min(...xs);
|
|
34
|
-
const maxX = Math.max(...xs);
|
|
35
|
-
const minY = Math.min(...ys);
|
|
36
|
-
const maxY = Math.max(...ys);
|
|
37
|
-
const centerX = (minX + maxX) / 2;
|
|
38
|
-
const centerY = (minY + maxY) / 2;
|
|
39
|
-
|
|
40
|
-
const sortByX = [...entries].sort((a, b) => a.pos.x - b.pos.x);
|
|
41
|
-
const sortByY = [...entries].sort((a, b) => a.pos.y - b.pos.y);
|
|
42
|
-
|
|
43
|
-
const out: Record<string, { x: number; y: number }> = {};
|
|
44
|
-
|
|
45
|
-
for (const e of entries) {
|
|
46
|
-
const next = { ...e.pos };
|
|
47
|
-
if (command === "left") next.x = minX;
|
|
48
|
-
if (command === "h-center") next.x = centerX;
|
|
49
|
-
if (command === "right") next.x = maxX;
|
|
50
|
-
if (command === "top") next.y = minY;
|
|
51
|
-
if (command === "v-center") next.y = centerY;
|
|
52
|
-
if (command === "bottom") next.y = maxY;
|
|
53
|
-
out[e.id] = next;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
if (command === "h-gap") {
|
|
57
|
-
const gap = entries.length > 1 ? (maxX - minX) / (entries.length - 1) : 0;
|
|
58
|
-
sortByX.forEach((e, index) => {
|
|
59
|
-
out[e.id] = { ...e.pos, x: minX + gap * index };
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
if (command === "v-gap") {
|
|
64
|
-
const gap = entries.length > 1 ? (maxY - minY) / (entries.length - 1) : 0;
|
|
65
|
-
sortByY.forEach((e, index) => {
|
|
66
|
-
out[e.id] = { ...e.pos, y: minY + gap * index };
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
return out;
|
|
71
|
-
}
|