@yanqirenshi/ihub.srv 0.2.2 → 0.2.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/CHANGELOG.md +20 -0
- package/README.md +3 -0
- package/bin/ihub-srv.mjs +3 -5
- package/package.json +1 -1
- package/src/tools/migrate-er-hierarchical.mjs +82 -19
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,26 @@
|
|
|
4
4
|
版は [semver](https://semver.org/lang/ja/) で付けます。0.x の間は、API・データのフォーマット・CLI を変えるときに、minor を上げます。
|
|
5
5
|
(リポジトリ全体の更新履歴は、[ルートの CHANGELOG.md](https://github.com/yanqirenshi/iHub/blob/main/CHANGELOG.md) にあります)
|
|
6
6
|
|
|
7
|
+
## 0.2.4 (2026-11)
|
|
8
|
+
|
|
9
|
+
- コードの変更は無い。(`@yanqirenshi/ihub.core` 0.2.4 と、版をそろえるだけ)
|
|
10
|
+
|
|
11
|
+
## 0.2.3 (2026-11)
|
|
12
|
+
|
|
13
|
+
### 修正
|
|
14
|
+
|
|
15
|
+
- `ihub-srv migrate-er` が、カラムのインスタンスの名前を、落とさないようにした。(yanqirenshi/iHub#100)
|
|
16
|
+
- これまでは、インスタンス (`column_instances.json`) の `physical_name`・`description` を捨て、マスタの名前を、COLUMN の `{ logical: name, physical: code }` にしていた。(例: `id` が `USER-ID` になる。同じ COLUMN を違う名前で 2 回使うテーブルは、どちらも同じ名前になる)
|
|
17
|
+
- いまは、インスタンスの名前 (`logical_name`・`physical_name`・`description`) と `data_type` が COLUMN と違うときは、名前の違うインスタンスごとに、別のマスタ (identifiers / attributes) を作って指す。
|
|
18
|
+
- COLUMN と同じ名前 (または名前が無い) インスタンスは、これまでどおり、COLUMN の 1 つのマスタを指す。
|
|
19
|
+
- 同じ COLUMN で同じ名前のインスタンスは、1 つのマスタを指す。
|
|
20
|
+
- 最初に現れた名前 (インスタンスの `_id` の順) のマスタが、COLUMN の `_id` を引き継ぎ、2 つ目からのマスタには、ER のどの `_id` とも重ならない値を振る。(何度変換しても、同じ結果になる)
|
|
21
|
+
- インスタンスの、写し先の決まっていない項目 (アプリが足した項目) は、`entities[].identifiers[]` / `attributes[]` の行に、そのまま写す。
|
|
22
|
+
- 変換のあとに、マスタの数 (COLUMN から作った数・名前の違いで増えた数) を表示する。
|
|
23
|
+
- 描画 (d3.er・model.er) は、変わらない。
|
|
24
|
+
- **0.2.2 以前の `migrate-er` で変換したアプリ**は、元のフラット形式のデータ (git の履歴) から、0.2.3 以降の `migrate-er` で変換し直すと、名前が戻る。
|
|
25
|
+
- 確かめた: iwo.Studio (インスタンス 132 件) と UkiyoSnow (202 件) の、変換前のフラット形式のデータで、すべてのインスタンスの名前が、変換のあとのマスタの名前と一致した。(`ihub-srv check` も通る)
|
|
26
|
+
|
|
7
27
|
## 0.2.2 (2026-11)
|
|
8
28
|
|
|
9
29
|
- コードの変更は無い。(`@yanqirenshi/ihub.core` 0.2.2 と、版をそろえるだけ)
|
package/README.md
CHANGED
|
@@ -114,6 +114,9 @@ ihub-srv migrate-er data
|
|
|
114
114
|
|
|
115
115
|
フラット形式の項目のうち、階層形式に写し先が決まっていないもの (TABLE の `color` など、アプリが足した項目) は、そのまま entity (COLUMN は identifiers / attributes) に写します。
|
|
116
116
|
|
|
117
|
+
カラムのインスタンスの名前 (`logical_name`・`physical_name`・`description`) と `data_type` は、COLUMN と違うときは、名前の違うインスタンスごとに、別のマスタ (identifiers / attributes) を作って指します。(階層形式では、名前はマスタだけが持つため。同じ COLUMN で同じ名前のインスタンスは、1 つのマスタを指す。最初の名前のマスタが COLUMN の `_id` を引き継ぎ、2 つ目からは、ER のどの `_id` とも重ならない値を振る) 変換のあとに、マスタの数 (COLUMN から作った数・名前の違いで増えた数) を表示します。
|
|
118
|
+
0.2.2 以前の `migrate-er` は、インスタンスの名前を、COLUMN の `name`・`code` に替えていました。0.2.3 以降で、元のフラット形式のデータから変換し直すと、名前が戻ります。
|
|
119
|
+
|
|
117
120
|
## createApp
|
|
118
121
|
|
|
119
122
|
```js
|
package/bin/ihub-srv.mjs
CHANGED
|
@@ -13,7 +13,7 @@ import { parseArgs } from 'node:util';
|
|
|
13
13
|
import { createApp, serve } from '../src/index.mjs';
|
|
14
14
|
import { createValidation } from '../src/validation/index.mjs';
|
|
15
15
|
import { convertLegacy } from '../src/tools/convert-legacy-data.mjs';
|
|
16
|
-
import { migrateDir } from '../src/tools/migrate-er-hierarchical.mjs';
|
|
16
|
+
import { migrateDir, summary } from '../src/tools/migrate-er-hierarchical.mjs';
|
|
17
17
|
|
|
18
18
|
const USAGE = `使い方:
|
|
19
19
|
ihub-srv [--data <dir>] [--static <dir>] [--port <n>] [--app-name <name>]
|
|
@@ -317,11 +317,9 @@ function migrateEr (args) {
|
|
|
317
317
|
}
|
|
318
318
|
|
|
319
319
|
try {
|
|
320
|
-
const data = migrateDir(resolve(args[0]));
|
|
320
|
+
const { data, stats } = migrateDir(resolve(args[0]));
|
|
321
321
|
|
|
322
|
-
console.log(`${args[0]}/er
|
|
323
|
-
` (identifiers ${data.identifiers.length}, attributes ${data.attributes.length},` +
|
|
324
|
-
` entities ${data.entities.length}, relationships ${data.relationships.length}。check: 問題なし)`);
|
|
322
|
+
console.log(`${args[0]}/er を階層形式にしました。 (${summary(data, stats)}。check: 問題なし)`);
|
|
325
323
|
|
|
326
324
|
return 0;
|
|
327
325
|
} catch (e) {
|
package/package.json
CHANGED
|
@@ -16,7 +16,15 @@
|
|
|
16
16
|
// 変換の規則:
|
|
17
17
|
// COLUMN → identifiers / attributes
|
|
18
18
|
// (column_type が IDENTIFIER のインスタンスが参照している COLUMN を identifiers に、それ以外を attributes に)
|
|
19
|
-
// name は { logical: name, physical: code }
|
|
19
|
+
// name は { logical: name, physical: code }。
|
|
20
|
+
// ただし、インスタンスが持つ名前 (logical_name・physical_name・description) と data_type が COLUMN と違うときは、
|
|
21
|
+
// その名前の組ごとに、別のマスタを作る。(階層形式では、名前はマスタだけが持ち、インスタンスはマスタを指すだけのため)
|
|
22
|
+
// - 同じ COLUMN の、同じ名前のインスタンスは、1 つのマスタを指す
|
|
23
|
+
// - COLUMN で最初に現れた名前 (インスタンスの _id の順) のマスタが、COLUMN の _id を引き継ぐ
|
|
24
|
+
// (どのインスタンスも COLUMN と同じ名前なら、いままでどおり COLUMN の _id・name・code のマスタ 1 つ)
|
|
25
|
+
// - 2 つ目からの名前のマスタには、ER のデータの中のどの _id とも重ならない id を、_id の順に振る (何度変換しても同じ)
|
|
26
|
+
// - 識別子 / 属性の振り分けは COLUMN ごと。(同じ COLUMN のマスタは、同じ側に入る)
|
|
27
|
+
// - インスタンスの、写し先の決まっていない項目 (アプリが足した項目) は、entities[].identifiers[] / attributes[] の行に写す
|
|
20
28
|
// TABLE → entities (position / size はそのまま)
|
|
21
29
|
// identifiers[] / attributes[] は、TABLE→COLUMN-INSTANCE の行と、COLUMN→COLUMN-INSTANCE (INSTANCE-OF) の行から組む
|
|
22
30
|
// INSTANCE-OF の行が無いインスタンスは、code の同じ COLUMN を参照する
|
|
@@ -36,6 +44,11 @@ const HIERARCHICAL_FILES = ['schemas', 'identifiers', 'attributes', 'entities',
|
|
|
36
44
|
|
|
37
45
|
// フラット形式のコレクション ({ entities, columns, column_instances, ports, relationships }) を、階層形式にする。
|
|
38
46
|
export function toHierarchical (flat) {
|
|
47
|
+
return convert(flat).data;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 階層形式にして、マスタの数 ({ columns: COLUMN の数, masters: 作ったマスタの数 }) も返す。
|
|
51
|
+
export function convert (flat) {
|
|
39
52
|
const { entities: tables, columns, column_instances: instances, ports, relationships: edges } = flat;
|
|
40
53
|
|
|
41
54
|
const byId = (list)=> new Map(list.map(d=> [d._id, d]));
|
|
@@ -79,22 +92,62 @@ export function toHierarchical (flat) {
|
|
|
79
92
|
const TABLE_KNOWN = ['_id', '_class', 'code', 'name', 'description', 'position', 'size'];
|
|
80
93
|
const rest = (d, known)=> Object.fromEntries(Object.entries(d).filter(([k])=> !known.includes(k)));
|
|
81
94
|
|
|
82
|
-
|
|
83
|
-
|
|
95
|
+
// インスタンスの項目のうち、マスタか、行の id・参照に写し先が決まっているもの。これ以外の項目 (アプリが足した項目) は、行にそのまま写す。
|
|
96
|
+
const INSTANCE_KNOWN = ['_id', '_class', 'code', 'physical_name', 'logical_name', 'description', 'column_type', 'data_type'];
|
|
97
|
+
|
|
98
|
+
const sorted = (list)=> [...list].sort((a, b)=> a._id - b._id);
|
|
99
|
+
|
|
100
|
+
// マスタの名前の組。(インスタンスの値が無い・空のときは、COLUMN の値)
|
|
101
|
+
const names_of = (instance, column)=> ({
|
|
102
|
+
logical: instance.logical_name || column.name,
|
|
103
|
+
physical: instance.physical_name || column.code,
|
|
104
|
+
description: instance.description || column.description || '',
|
|
105
|
+
data_type: instance.data_type || column.data_type || '',
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// COLUMN ごとの、名前の組 → マスタの id。(インスタンスの _id の順に、最初に現れた名前が、COLUMN の _id を引き継ぐ)
|
|
109
|
+
const masters_of = new Map(columns.map(c=> [c._id, new Map()]));
|
|
110
|
+
const master_of = new Map(); // インスタンス → マスタの id
|
|
111
|
+
const column_by_id = byId(columns);
|
|
112
|
+
|
|
113
|
+
// 2 つ目からの名前のマスタの id。ER のデータの中のどの _id とも重ならない値から、順に振る。
|
|
114
|
+
let next_id = Math.max(0, ...[tables, columns, instances, ports, edges].flat().map(d=> d._id).filter(Number.isFinite)) + 1;
|
|
115
|
+
|
|
116
|
+
for (const instance of sorted(instances)) {
|
|
117
|
+
const column = column_by_id.get(column_of.get(instance._id));
|
|
118
|
+
const names = names_of(instance, column);
|
|
119
|
+
const key = JSON.stringify([names.logical, names.physical, names.description, names.data_type]);
|
|
120
|
+
const known = masters_of.get(column._id);
|
|
121
|
+
|
|
122
|
+
if (!known.has(key))
|
|
123
|
+
known.set(key, { id: known.size===0 ? column._id : next_id++, names });
|
|
84
124
|
|
|
85
|
-
|
|
86
|
-
|
|
125
|
+
master_of.set(instance._id, known.get(key).id);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const master = (column, id, names)=> {
|
|
129
|
+
const out = { id, name: { logical: names.logical, physical: names.physical } };
|
|
130
|
+
|
|
131
|
+
if (names.description)
|
|
132
|
+
out.description = names.description;
|
|
87
133
|
|
|
88
|
-
if (
|
|
89
|
-
out.data_type =
|
|
134
|
+
if (names.data_type)
|
|
135
|
+
out.data_type = names.data_type;
|
|
90
136
|
|
|
91
137
|
return { ...out, ...rest(column, COLUMN_KNOWN) };
|
|
92
138
|
};
|
|
93
139
|
|
|
94
|
-
|
|
140
|
+
// COLUMN ごとのマスタ。(インスタンスが 1 つも無い COLUMN は、COLUMN の名前のマスタ 1 つ)
|
|
141
|
+
const masters_for = (column)=> {
|
|
142
|
+
const known = [...masters_of.get(column._id).values()];
|
|
143
|
+
|
|
144
|
+
return known.length > 0
|
|
145
|
+
? known.map(m=> master(column, m.id, m.names))
|
|
146
|
+
: [master(column, column._id, { logical: column.name, physical: column.code, description: column.description || '', data_type: column.data_type || '' })];
|
|
147
|
+
};
|
|
95
148
|
|
|
96
|
-
const identifiers = sorted(columns.filter(c=> identifier_columns.has(c._id))).
|
|
97
|
-
const attributes = sorted(columns.filter(c=> !identifier_columns.has(c._id))).
|
|
149
|
+
const identifiers = sorted(columns.filter(c=> identifier_columns.has(c._id))).flatMap(masters_for);
|
|
150
|
+
const attributes = sorted(columns.filter(c=> !identifier_columns.has(c._id))).flatMap(masters_for);
|
|
98
151
|
|
|
99
152
|
const entities = tables.map(table=> {
|
|
100
153
|
const mine = sorted(instances.filter(d=> table_of.get(d._id)===table._id));
|
|
@@ -108,9 +161,9 @@ export function toHierarchical (flat) {
|
|
|
108
161
|
size: table.size,
|
|
109
162
|
...rest(table, TABLE_KNOWN),
|
|
110
163
|
identifiers: mine.filter(d=> identifier_columns.has(column_of.get(d._id)))
|
|
111
|
-
.map(d=> ({ id: d._id, identifier:
|
|
164
|
+
.map(d=> ({ id: d._id, identifier: master_of.get(d._id), ...rest(d, INSTANCE_KNOWN) })),
|
|
112
165
|
attributes: mine.filter(d=> !identifier_columns.has(column_of.get(d._id)))
|
|
113
|
-
.map(d=> ({ id: d._id, attribute:
|
|
166
|
+
.map(d=> ({ id: d._id, attribute: master_of.get(d._id), ...rest(d, INSTANCE_KNOWN) })),
|
|
114
167
|
};
|
|
115
168
|
});
|
|
116
169
|
|
|
@@ -159,7 +212,19 @@ export function toHierarchical (flat) {
|
|
|
159
212
|
return out;
|
|
160
213
|
});
|
|
161
214
|
|
|
162
|
-
|
|
215
|
+
const data = { schemas: [], identifiers, attributes, entities, relationships };
|
|
216
|
+
|
|
217
|
+
return { data, stats: { columns: columns.length, masters: identifiers.length + attributes.length } };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// 変換した結果の説明。(マスタの数と、名前の違いで増えた数)
|
|
221
|
+
export function summary (data, stats) {
|
|
222
|
+
const added = stats.masters - stats.columns;
|
|
223
|
+
|
|
224
|
+
return `identifiers ${data.identifiers.length}, attributes ${data.attributes.length},` +
|
|
225
|
+
` entities ${data.entities.length}, relationships ${data.relationships.length}。` +
|
|
226
|
+
`マスタは、COLUMN から ${stats.columns} 件、名前の違いで増えたもの ${added} 件` +
|
|
227
|
+
` (COLUMN と名前の違うインスタンスごとに、別のマスタを作ります)`;
|
|
163
228
|
}
|
|
164
229
|
|
|
165
230
|
// API サーバーと同じ書き方 (4 つの空白で字下げし、最後に改行) で書く。
|
|
@@ -174,7 +239,7 @@ export function migrateDir (dir) {
|
|
|
174
239
|
throw new Error(`${er}: フラット形式ではありません。(columns.json がありません。変換済みです)`);
|
|
175
240
|
|
|
176
241
|
const flat = Object.fromEntries(FLAT_FILES.map(name=> [name, JSON.parse(readFileSync(join(er, `${name}.json`), 'utf8'))]));
|
|
177
|
-
const data =
|
|
242
|
+
const { data, stats } = convert(flat);
|
|
178
243
|
|
|
179
244
|
const errors = check(data, 'er/');
|
|
180
245
|
|
|
@@ -188,7 +253,7 @@ export function migrateDir (dir) {
|
|
|
188
253
|
if (!HIERARCHICAL_FILES.includes(name))
|
|
189
254
|
unlinkSync(join(er, `${name}.json`));
|
|
190
255
|
|
|
191
|
-
return data;
|
|
256
|
+
return { data, stats };
|
|
192
257
|
}
|
|
193
258
|
|
|
194
259
|
if (process.argv[1] && resolve(process.argv[1])===fileURLToPath(import.meta.url)) {
|
|
@@ -200,11 +265,9 @@ if (process.argv[1] && resolve(process.argv[1])===fileURLToPath(import.meta.url)
|
|
|
200
265
|
}
|
|
201
266
|
|
|
202
267
|
try {
|
|
203
|
-
const data = migrateDir(resolve(dir));
|
|
268
|
+
const { data, stats } = migrateDir(resolve(dir));
|
|
204
269
|
|
|
205
|
-
console.log(`${dir}/er
|
|
206
|
-
` (identifiers ${data.identifiers.length}, attributes ${data.attributes.length},` +
|
|
207
|
-
` entities ${data.entities.length}, relationships ${data.relationships.length}。check: 問題なし)`);
|
|
270
|
+
console.log(`${dir}/er を階層形式にしました。 (${summary(data, stats)}。check: 問題なし)`);
|
|
208
271
|
} catch (e) {
|
|
209
272
|
console.error(e.message);
|
|
210
273
|
process.exit(1);
|