@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yanqirenshi/ihub.srv",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "iHub の API サーバー。data/ の JSON を、スキーマとルールで検証しながら読み書きする。",
5
5
  "keywords": [
6
6
  "ihub",
@@ -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
- const master = (column)=> {
83
- const out = { id: column._id, name: { logical: column.name, physical: column.code } };
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
- if (column.description)
86
- out.description = column.description;
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 (column.data_type)
89
- out.data_type = column.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
- const sorted = (list)=> [...list].sort((a, b)=> a._id - b._id);
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))).map(master);
97
- const attributes = sorted(columns.filter(c=> !identifier_columns.has(c._id))).map(master);
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: column_of.get(d._id) })),
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: column_of.get(d._id) })),
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
- return { schemas: [], identifiers, attributes, entities, relationships };
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 = toHierarchical(flat);
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);