@yanqirenshi/model.core 0.0.0-stage → 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2018-2026 Satoshi Iwasaki (yanqirenshi@gmail.com)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,67 @@
1
- # Temporary Holding Version
1
+ # @yanqirenshi/model.core
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ 図ごとのモデル(`@yanqirenshi/model.*`)が共通で使う部品です。
4
+ react / d3 に依存しない、純粋な関数だけを持ちます。
5
+
6
+ | 持つもの | 入口 |
7
+ |---|---|
8
+ | 入力の検証エラー(利用者向けのメッセージ) | `ModelError` |
9
+ | JSON Merge Patch(RFC 7386)の道具 | `mergePatch(record, patch)` / `getByPath(obj, path)` / `put(patch, path, value)` |
10
+ | ドラッグの移動量を position の patch にする | `movePatch(record, move, collection)` |
11
+ | インスペクタの入力のうち、描いている値から変わった項目だけ | `changedValues(effective, values, fields)` |
12
+ | インスペクタの入力(文字列)の読み取り | `isColor` / `toColor` / `toWidth` / `toLength` / `toDegree` / `toOption` / `toOptionNumber` / `toOptionNumberOrNull` / `toBoolean` / `toOptionalText` / `toNumber` |
13
+ | ノードと線の図(graph 系)に共通の整合性ルール | `checkGraph(data, prefix)` / `flattenNodes` / `checkUnique` / `findById` |
14
+
15
+ ## 位置づけ
16
+
17
+ ```
18
+ 利用側(iHub / デモ) ──▶ model.unchi / model.er / model.tm / model.sitemap / model.deployment ──▶ model.core
19
+ ```
20
+
21
+ **利用側がこのパッケージを直接 import する必要はありません。**
22
+ 各 model.* が `ModelError` / `mergePatch` / `getByPath` / `isColor` を再 export し、`check` / `movePatch` には自分の型を付けて公開しています。
23
+ 図ごとの知識(JSON Schema・`withDefaults`・`fieldsOf`・図ごとの `check`・`applyPatch`)は各 model.* にあり、ここには置きません。
24
+
25
+ [Foolsgolds/Assholes#151](https://github.com/Foolsgolds/Assholes/issues/151) で、5 つの model.* に複製されていた部分を切り出したものです。
26
+
27
+ ## 各 model.* から使うとき
28
+
29
+ ```ts
30
+ import {
31
+ ModelError, changedValues, movePatch, put, toColor, toLength, toWidth,
32
+ } from '@yanqirenshi/model.core';
33
+
34
+ // 読み取り部品は、図ごとの規則の違いを引数で受ける
35
+ toWidth(value, '枠線の太さ'); // 0 より大きい数(0 は view が既定に倒すとき)
36
+ toWidth(value, '枠線の太さ', { allowZero: true }); // 0 以上(0 = 線なし)
37
+ toLength(value, '幅', { min: 1 }); // 「幅は 1 以上の数で入力してください。」
38
+ toLength(value, '幅', { hint: '(0 で子に合わせる)' });
39
+
40
+ // ドラッグの終わり → patch(collection は図ごとに固定して公開する)
41
+ export function movePatchForMyDiagram(node, move) { return movePatch(node, move, 'nodes'); }
42
+
43
+ // 適用 → 変わった項目だけを読み取って patch にする
44
+ for (const [path, value] of changedValues(withDefaults(record), values, fieldsOf(record))) {
45
+ switch (path) {
46
+ case 'border.color': put(patch, path, toColor(value, '枠線の色')); break;
47
+ // ...
48
+ }
49
+ }
50
+ ```
51
+
52
+ 読み取り部品が投げる `ModelError` の文言は、各 model.* のテストが見ているものと同じです(`tests/read.test.ts`)。
53
+
54
+ ## 配布形式
55
+
56
+ ESM(`dist/`)に加えて CommonJS 版(`dist/cjs/`)も出します。
57
+ 各 model.* の CommonJS 版(Babel 系の view を jest で試験するためのもの)がこれを `require` するためで、すべての model.* が CommonJS 版を出さなくなったら外せます。
58
+
59
+ ## コマンド
60
+
61
+ ```bash
62
+ npm run build -w @yanqirenshi/model.core # tsc で dist/(ESM + d.ts)と dist/cjs/(CommonJS)へビルド
63
+ npm run typecheck -w @yanqirenshi/model.core
64
+ npm test -w @yanqirenshi/model.core # vitest
65
+ ```
66
+
67
+ 各 model.* のビルド・テスト・Babel 系 view の jest は、このパッケージの `dist/` を先に作っておく必要があります(`npm install` の `prepare` で作られます)。
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ModelError = void 0;
4
+ /**
5
+ * 入力の検証エラー。message は利用者にそのまま見せられる日本語。
6
+ * 各 model.* はこれを再 export するので、どの model.* の ModelError も同じクラス(instanceof で判別できる)。
7
+ * 使い方の誤り(マスタなのに collection を渡していない、など)はこれではなく Error にする。
8
+ */
9
+ class ModelError extends Error {
10
+ constructor(message) {
11
+ super(message);
12
+ this.name = 'ModelError';
13
+ }
14
+ }
15
+ exports.ModelError = ModelError;
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ // コレクションをまたぐ整合性の検査と、レコードの検索。
3
+ //
4
+ // checkGraph は、ノードと線の図(graph 系: unchi / sitemap / deployment)に共通の検査で、
5
+ // iHub の server/validation/rules.mjs の checkGraph と同じ検査・同じ文言。
6
+ // checkUnique / findById は階層形式(er / tm)の check からも使う。
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.flattenNodes = flattenNodes;
9
+ exports.checkUnique = checkUnique;
10
+ exports.checkGraph = checkGraph;
11
+ exports.findById = findById;
12
+ /** 入れ子のノード(children)を平らにする(親 → 子 の順) */
13
+ function flattenNodes(nodes) {
14
+ return nodes.flatMap(node => [node, ...flattenNodes((node.children || []))]);
15
+ }
16
+ /** list の中で id が重複していれば errors に足す。id の集合を返す */
17
+ function checkUnique(errors, label, list) {
18
+ const values = new Set();
19
+ for (const d of list) {
20
+ if (values.has(d.id))
21
+ errors.push(`${label}: id が重複しています。(id=${String(d.id)})`);
22
+ values.add(d.id);
23
+ }
24
+ return values;
25
+ }
26
+ /**
27
+ * ノードと線の図を検査する。戻り値はエラーのメッセージの配列(空なら問題なし)。
28
+ * - nodes(子を含む)の id が重複していない
29
+ * - edges の id が重複していない
30
+ * - edges の from.id / to.id がノードに存在する
31
+ *
32
+ * @param prefix メッセージのコレクション名の前に付ける文字列(例: 'overview/' → 'overview/nodes: ...')
33
+ */
34
+ function checkGraph(data, prefix = '') {
35
+ const errors = [];
36
+ const node_ids = checkUnique(errors, `${prefix}nodes`, flattenNodes(data.nodes || []));
37
+ checkUnique(errors, `${prefix}edges`, data.edges || []);
38
+ for (const edge of data.edges || [])
39
+ for (const end of ['from', 'to'])
40
+ if (!edge[end] || !node_ids.has(edge[end].id))
41
+ errors.push(`${prefix}edges: ${end}.id のノードが見つかりません。(id=${String(edge.id)}, ${end}.id=${String(edge[end]?.id)})`);
42
+ return errors;
43
+ }
44
+ /** レコードを id で探す(無ければ null)。children があれば子も探す */
45
+ function findById(list, id) {
46
+ for (const d of list || []) {
47
+ if (d.id === id)
48
+ return d;
49
+ const children = d.children;
50
+ const hit = children ? findById(children, id) : null;
51
+ if (hit)
52
+ return hit;
53
+ }
54
+ return null;
55
+ }
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ // model.core — 図ごとのモデル(@yanqirenshi/model.*)が共通で使う部品。描画はしない(react / d3 に依存しない)。
3
+ //
4
+ // エラー: ModelError(利用者向けのメッセージを持つ検証エラー)
5
+ // patch の道具: getByPath / mergePatch(JSON Merge Patch)/ put / movePatch(record, move, collection) / changedValues
6
+ // 入力の読み取り: isColor / toNumber / toColor / toWidth / toLength / toDegree / toOption / toOptionNumber /
7
+ // toOptionNumberOrNull / toBoolean / toOptionalText
8
+ // graph 系の検査: checkGraph(data, prefix) / flattenNodes / checkUnique / findById
9
+ //
10
+ // 図ごとの知識(スキーマ・withDefaults・fieldsOf・図ごとの check・applyPatch)は各 model.* にある。
11
+ // 利用側(iHub やデモ)は各 model.* を読めばよく、このパッケージを直接 import する必要は無い
12
+ // (各 model.* が ModelError / mergePatch / getByPath などを再 export する)。
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.findById = exports.checkGraph = exports.checkUnique = exports.flattenNodes = exports.toOptionalText = exports.toBoolean = exports.toOptionNumberOrNull = exports.toOptionNumber = exports.toOption = exports.toDegree = exports.toLength = exports.toWidth = exports.toColor = exports.isColor = exports.toNumber = exports.changedValues = exports.movePatch = exports.put = exports.mergePatch = exports.getByPath = exports.ModelError = void 0;
15
+ var error_js_1 = require("./error.js");
16
+ Object.defineProperty(exports, "ModelError", { enumerable: true, get: function () { return error_js_1.ModelError; } });
17
+ var patch_js_1 = require("./patch.js");
18
+ Object.defineProperty(exports, "getByPath", { enumerable: true, get: function () { return patch_js_1.getByPath; } });
19
+ Object.defineProperty(exports, "mergePatch", { enumerable: true, get: function () { return patch_js_1.mergePatch; } });
20
+ Object.defineProperty(exports, "put", { enumerable: true, get: function () { return patch_js_1.put; } });
21
+ Object.defineProperty(exports, "movePatch", { enumerable: true, get: function () { return patch_js_1.movePatch; } });
22
+ Object.defineProperty(exports, "changedValues", { enumerable: true, get: function () { return patch_js_1.changedValues; } });
23
+ var read_js_1 = require("./read.js");
24
+ Object.defineProperty(exports, "toNumber", { enumerable: true, get: function () { return read_js_1.toNumber; } });
25
+ Object.defineProperty(exports, "isColor", { enumerable: true, get: function () { return read_js_1.isColor; } });
26
+ Object.defineProperty(exports, "toColor", { enumerable: true, get: function () { return read_js_1.toColor; } });
27
+ Object.defineProperty(exports, "toWidth", { enumerable: true, get: function () { return read_js_1.toWidth; } });
28
+ Object.defineProperty(exports, "toLength", { enumerable: true, get: function () { return read_js_1.toLength; } });
29
+ Object.defineProperty(exports, "toDegree", { enumerable: true, get: function () { return read_js_1.toDegree; } });
30
+ Object.defineProperty(exports, "toOption", { enumerable: true, get: function () { return read_js_1.toOption; } });
31
+ Object.defineProperty(exports, "toOptionNumber", { enumerable: true, get: function () { return read_js_1.toOptionNumber; } });
32
+ Object.defineProperty(exports, "toOptionNumberOrNull", { enumerable: true, get: function () { return read_js_1.toOptionNumberOrNull; } });
33
+ Object.defineProperty(exports, "toBoolean", { enumerable: true, get: function () { return read_js_1.toBoolean; } });
34
+ Object.defineProperty(exports, "toOptionalText", { enumerable: true, get: function () { return read_js_1.toOptionalText; } });
35
+ var graph_js_1 = require("./graph.js");
36
+ Object.defineProperty(exports, "flattenNodes", { enumerable: true, get: function () { return graph_js_1.flattenNodes; } });
37
+ Object.defineProperty(exports, "checkUnique", { enumerable: true, get: function () { return graph_js_1.checkUnique; } });
38
+ Object.defineProperty(exports, "checkGraph", { enumerable: true, get: function () { return graph_js_1.checkGraph; } });
39
+ Object.defineProperty(exports, "findById", { enumerable: true, get: function () { return graph_js_1.findById; } });
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "commonjs"
3
+ }
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+ // patch(JSON Merge Patch)を扱う道具。各 model.* の applyPatch / movePatch はこれを組み合わせて作る。
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.getByPath = getByPath;
5
+ exports.mergePatch = mergePatch;
6
+ exports.put = put;
7
+ exports.movePatch = movePatch;
8
+ exports.changedValues = changedValues;
9
+ /** ドット区切りの path で値を取り出す */
10
+ function getByPath(obj, path) {
11
+ return path.split('.').reduce((acc, key) => (acc == null ? acc : acc[key]), obj);
12
+ }
13
+ /**
14
+ * JSON Merge Patch をレコードに重ねたものを返す(入力は書き換えない)。
15
+ * null の項目は除く。保存したあと、表示を先に更新するときに使う。
16
+ */
17
+ function mergePatch(record, patch) {
18
+ const out = { ...record };
19
+ for (const [key, value] of Object.entries(patch)) {
20
+ if (value === null) {
21
+ delete out[key];
22
+ }
23
+ else if (typeof value === 'object' && !Array.isArray(value)) {
24
+ const base = out[key];
25
+ out[key] = mergePatch(base && typeof base === 'object' && !Array.isArray(base) ? base : {}, value);
26
+ }
27
+ else {
28
+ out[key] = value;
29
+ }
30
+ }
31
+ return out;
32
+ }
33
+ /** patch の入れ子に値を置く(put(patch, 'border.color', v) で patch.border.color = v) */
34
+ function put(patch, path, value) {
35
+ const keys = path.split('.');
36
+ let cur = patch;
37
+ for (const key of keys.slice(0, -1)) {
38
+ if (!cur[key] || typeof cur[key] !== 'object')
39
+ cur[key] = {};
40
+ cur = cur[key];
41
+ }
42
+ cur[keys[keys.length - 1]] = value;
43
+ }
44
+ /**
45
+ * ドラッグで動かしたレコードの位置を保存する patch。
46
+ * 保存してある position に移動量を足し、整数に丸める(position が無いときは 0, 0 から)。x / y 以外(z など)は触らない。
47
+ * 入れ子のノードの position は親の中での位置なので、親を動かしても子の patch は要らない。
48
+ *
49
+ * @param collection 保存先(graph 系は 'nodes'、階層形式は 'entities')
50
+ */
51
+ function movePatch(record, move, collection) {
52
+ const base = record.position || { x: 0, y: 0 };
53
+ return {
54
+ collection,
55
+ id: record.id,
56
+ patch: {
57
+ position: {
58
+ x: Math.round(base.x + move.dx),
59
+ y: Math.round(base.y + move.dy),
60
+ },
61
+ },
62
+ };
63
+ }
64
+ /**
65
+ * インスペクタの入力のうち、図に描いている値(effective = withDefaults で埋めたレコード)から変わった項目だけを返す。
66
+ * 前後の空白は除く。読み取り専用の項目と、values に無い項目は含めない。
67
+ */
68
+ function changedValues(effective, values, fields) {
69
+ const out = new Map();
70
+ for (const field of fields) {
71
+ if (field.type === 'readonly')
72
+ continue;
73
+ const value = values[field.path];
74
+ if (value === undefined)
75
+ continue;
76
+ const current = getByPath(effective, field.path);
77
+ if (value.trim() !== String(current ?? ''))
78
+ out.set(field.path, value.trim());
79
+ }
80
+ return out;
81
+ }
@@ -0,0 +1,108 @@
1
+ "use strict";
2
+ // インスペクタの入力(文字列)を、保存する値に読み取る部品。
3
+ // 読めないときは ModelError を投げる(メッセージは利用者にそのまま見せられる日本語。label は項目の名前)。
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.toNumber = toNumber;
6
+ exports.isColor = isColor;
7
+ exports.toColor = toColor;
8
+ exports.toWidth = toWidth;
9
+ exports.toLength = toLength;
10
+ exports.toDegree = toDegree;
11
+ exports.toOption = toOption;
12
+ exports.toOptionNumber = toOptionNumber;
13
+ exports.toOptionNumberOrNull = toOptionNumberOrNull;
14
+ exports.toBoolean = toBoolean;
15
+ exports.toOptionalText = toOptionalText;
16
+ const error_js_1 = require("./error.js");
17
+ /** 入力した文字列を数にする。数でない(空を含む)ときは null */
18
+ function toNumber(value) {
19
+ const n = Number(value);
20
+ return value !== '' && Number.isFinite(n) ? n : null;
21
+ }
22
+ /**
23
+ * 色として読める文字列か。
24
+ * ブラウザでは CSS.supports('color', v) で判定する。Node など CSS の無い環境では、
25
+ * #rgb / #rrggbb / #rrggbbaa・rgb() などの関数表記・英字だけの色名、を通す緩い判定にする
26
+ * (色名の一覧は持たない。環境で判定が変わる)。
27
+ */
28
+ function isColor(value) {
29
+ if (value === '')
30
+ return false;
31
+ const css = globalThis.CSS;
32
+ if (css && typeof css.supports === 'function')
33
+ return css.supports('color', value);
34
+ return /^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.test(value)
35
+ || /^(rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch|color)\(.+\)$/i.test(value)
36
+ || /^[a-z]+$/i.test(value);
37
+ }
38
+ /** 色(isColor) */
39
+ function toColor(value, label) {
40
+ if (!isColor(value))
41
+ throw new error_js_1.ModelError(`${label}は、色 (#ff0000 や red など) で入力してください。`);
42
+ return value;
43
+ }
44
+ /**
45
+ * 線の太さ。既定は 0 より大きい数(view が 0 を「書いていない」と見て既定に倒すとき)。
46
+ * allowZero で 0 以上(0 = 線なし)にできる。整数には丸めない。
47
+ */
48
+ function toWidth(value, label, options = {}) {
49
+ const n = toNumber(value);
50
+ if (options.allowZero) {
51
+ if (n === null || n < 0)
52
+ throw new error_js_1.ModelError(`${label}は、0 以上の数で入力してください。`);
53
+ }
54
+ else if (n === null || n <= 0) {
55
+ throw new error_js_1.ModelError(`${label}は、0 より大きい数で入力してください。`);
56
+ }
57
+ return n;
58
+ }
59
+ /**
60
+ * 幅・高さ(min 以上の整数に丸める)。
61
+ * min は既定 0。0 に意味があるとき(「子に合わせる」など)は hint に書くとメッセージの末尾に付く。
62
+ */
63
+ function toLength(value, label, options = {}) {
64
+ const min = options.min ?? 0;
65
+ const n = toNumber(value);
66
+ if (n === null || n < min)
67
+ throw new error_js_1.ModelError(`${label}は ${min} 以上の数で入力してください。${options.hint ?? ''}`);
68
+ return Math.round(n);
69
+ }
70
+ /** 角度(0 以上 360 未満の整数に正規化) */
71
+ function toDegree(value, label) {
72
+ const n = toNumber(value);
73
+ if (n === null)
74
+ throw new error_js_1.ModelError(`${label}は、角度 (数) で入力してください。`);
75
+ return ((Math.round(n) % 360) + 360) % 360;
76
+ }
77
+ /** 候補の中の文字列 */
78
+ function toOption(value, options, label) {
79
+ if (!options.includes(value))
80
+ throw new error_js_1.ModelError(`${label}は、${options.join(' / ')} のどれかで入力してください。`);
81
+ return value;
82
+ }
83
+ /** 候補の中の数(カーディナリティ・オプショナリティなど) */
84
+ function toOptionNumber(value, options, label) {
85
+ const n = toNumber(value);
86
+ if (n === null || !options.includes(n))
87
+ throw new error_js_1.ModelError(`${label}は、${options.join(' / ')} のどれかで入力してください。`);
88
+ return n;
89
+ }
90
+ /** 候補の中の数、または空(null = 項目を除く) */
91
+ function toOptionNumberOrNull(value, options, label) {
92
+ if (value === '')
93
+ return null;
94
+ const n = toNumber(value);
95
+ if (n === null || !options.includes(n))
96
+ throw new error_js_1.ModelError(`${label}は、${options.join(' / ')} のどれか (または空) で入力してください。`);
97
+ return n;
98
+ }
99
+ /** true / false */
100
+ function toBoolean(value, label) {
101
+ if (value !== 'true' && value !== 'false')
102
+ throw new error_js_1.ModelError(`${label}は、true / false のどちらかで入力してください。`);
103
+ return value === 'true';
104
+ }
105
+ /** 空にできる文字列。空なら null(JSON Merge Patch で項目を除く) */
106
+ function toOptionalText(value) {
107
+ return value === '' ? null : value;
108
+ }
@@ -0,0 +1,4 @@
1
+ "use strict";
2
+ // model.core の公開型。各 model.*(unchi / er / tm / sitemap / deployment)はこれをエイリアスで公開する
3
+ // (UnchiMove = ModelMove のように。公開 API 名は各 model.* のまま)。
4
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,9 @@
1
+ /**
2
+ * 入力の検証エラー。message は利用者にそのまま見せられる日本語。
3
+ * 各 model.* はこれを再 export するので、どの model.* の ModelError も同じクラス(instanceof で判別できる)。
4
+ * 使い方の誤り(マスタなのに collection を渡していない、など)はこれではなく Error にする。
5
+ */
6
+ export declare class ModelError extends Error {
7
+ constructor(message: string);
8
+ }
9
+ //# sourceMappingURL=error.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error.d.ts","sourceRoot":"","sources":["../src/error.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,qBAAa,UAAW,SAAQ,KAAK;gBACvB,OAAO,EAAE,MAAM;CAI5B"}
package/dist/error.js ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * 入力の検証エラー。message は利用者にそのまま見せられる日本語。
3
+ * 各 model.* はこれを再 export するので、どの model.* の ModelError も同じクラス(instanceof で判別できる)。
4
+ * 使い方の誤り(マスタなのに collection を渡していない、など)はこれではなく Error にする。
5
+ */
6
+ export class ModelError extends Error {
7
+ constructor(message) {
8
+ super(message);
9
+ this.name = 'ModelError';
10
+ }
11
+ }
@@ -0,0 +1,21 @@
1
+ import type { Graph, GraphNode, ModelId } from './types.js';
2
+ /** 入れ子のノード(children)を平らにする(親 → 子 の順) */
3
+ export declare function flattenNodes<N extends GraphNode>(nodes: readonly N[]): N[];
4
+ /** list の中で id が重複していれば errors に足す。id の集合を返す */
5
+ export declare function checkUnique(errors: string[], label: string, list: readonly {
6
+ id: ModelId;
7
+ }[]): Set<ModelId>;
8
+ /**
9
+ * ノードと線の図を検査する。戻り値はエラーのメッセージの配列(空なら問題なし)。
10
+ * - nodes(子を含む)の id が重複していない
11
+ * - edges の id が重複していない
12
+ * - edges の from.id / to.id がノードに存在する
13
+ *
14
+ * @param prefix メッセージのコレクション名の前に付ける文字列(例: 'overview/' → 'overview/nodes: ...')
15
+ */
16
+ export declare function checkGraph(data: Graph, prefix?: string): string[];
17
+ /** レコードを id で探す(無ければ null)。children があれば子も探す */
18
+ export declare function findById<T extends {
19
+ id: ModelId;
20
+ }>(list: readonly T[] | null | undefined, id: ModelId): T | null;
21
+ //# sourceMappingURL=graph.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graph.d.ts","sourceRoot":"","sources":["../src/graph.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAE5D,wCAAwC;AACxC,wBAAgB,YAAY,CAAC,CAAC,SAAS,SAAS,EAAE,KAAK,EAAE,SAAS,CAAC,EAAE,GAAG,CAAC,EAAE,CAE1E;AAED,gDAAgD;AAChD,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS;IAAE,EAAE,EAAE,OAAO,CAAA;CAAE,EAAE,GAAG,GAAG,CAAC,OAAO,CAAC,CAW3G;AAED;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,KAAK,EAAE,MAAM,SAAK,GAAG,MAAM,EAAE,CAa7D;AAED,gDAAgD;AAChD,wBAAgB,QAAQ,CAAC,CAAC,SAAS;IAAE,EAAE,EAAE,OAAO,CAAA;CAAE,EAAE,IAAI,EAAE,SAAS,CAAC,EAAE,GAAG,IAAI,GAAG,SAAS,EAAE,EAAE,EAAE,OAAO,GAAG,CAAC,GAAG,IAAI,CAahH"}
package/dist/graph.js ADDED
@@ -0,0 +1,49 @@
1
+ // コレクションをまたぐ整合性の検査と、レコードの検索。
2
+ //
3
+ // checkGraph は、ノードと線の図(graph 系: unchi / sitemap / deployment)に共通の検査で、
4
+ // iHub の server/validation/rules.mjs の checkGraph と同じ検査・同じ文言。
5
+ // checkUnique / findById は階層形式(er / tm)の check からも使う。
6
+ /** 入れ子のノード(children)を平らにする(親 → 子 の順) */
7
+ export function flattenNodes(nodes) {
8
+ return nodes.flatMap(node => [node, ...flattenNodes((node.children || []))]);
9
+ }
10
+ /** list の中で id が重複していれば errors に足す。id の集合を返す */
11
+ export function checkUnique(errors, label, list) {
12
+ const values = new Set();
13
+ for (const d of list) {
14
+ if (values.has(d.id))
15
+ errors.push(`${label}: id が重複しています。(id=${String(d.id)})`);
16
+ values.add(d.id);
17
+ }
18
+ return values;
19
+ }
20
+ /**
21
+ * ノードと線の図を検査する。戻り値はエラーのメッセージの配列(空なら問題なし)。
22
+ * - nodes(子を含む)の id が重複していない
23
+ * - edges の id が重複していない
24
+ * - edges の from.id / to.id がノードに存在する
25
+ *
26
+ * @param prefix メッセージのコレクション名の前に付ける文字列(例: 'overview/' → 'overview/nodes: ...')
27
+ */
28
+ export function checkGraph(data, prefix = '') {
29
+ const errors = [];
30
+ const node_ids = checkUnique(errors, `${prefix}nodes`, flattenNodes(data.nodes || []));
31
+ checkUnique(errors, `${prefix}edges`, data.edges || []);
32
+ for (const edge of data.edges || [])
33
+ for (const end of ['from', 'to'])
34
+ if (!edge[end] || !node_ids.has(edge[end].id))
35
+ errors.push(`${prefix}edges: ${end}.id のノードが見つかりません。(id=${String(edge.id)}, ${end}.id=${String(edge[end]?.id)})`);
36
+ return errors;
37
+ }
38
+ /** レコードを id で探す(無ければ null)。children があれば子も探す */
39
+ export function findById(list, id) {
40
+ for (const d of list || []) {
41
+ if (d.id === id)
42
+ return d;
43
+ const children = d.children;
44
+ const hit = children ? findById(children, id) : null;
45
+ if (hit)
46
+ return hit;
47
+ }
48
+ return null;
49
+ }
@@ -0,0 +1,6 @@
1
+ export type { ModelId, MergePatch, ModelChange, ModelMove, ModelFieldType, ModelField, ModelValues, Positioned, GraphNode, GraphEdge, Graph, } from './types.js';
2
+ export { ModelError } from './error.js';
3
+ export { getByPath, mergePatch, put, movePatch, changedValues } from './patch.js';
4
+ export { toNumber, isColor, toColor, toWidth, toLength, toDegree, toOption, toOptionNumber, toOptionNumberOrNull, toBoolean, toOptionalText, } from './read.js';
5
+ export { flattenNodes, checkUnique, checkGraph, findById } from './graph.js';
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAYA,YAAY,EACV,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,SAAS,EAAE,cAAc,EAAE,UAAU,EAAE,WAAW,EAAE,UAAU,EAChG,SAAS,EAAE,SAAS,EAAE,KAAK,GAC5B,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,GAAG,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAClF,OAAO,EACL,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,cAAc,EAAE,oBAAoB,EAAE,SAAS,EAClH,cAAc,GACf,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,15 @@
1
+ // model.core — 図ごとのモデル(@yanqirenshi/model.*)が共通で使う部品。描画はしない(react / d3 に依存しない)。
2
+ //
3
+ // エラー: ModelError(利用者向けのメッセージを持つ検証エラー)
4
+ // patch の道具: getByPath / mergePatch(JSON Merge Patch)/ put / movePatch(record, move, collection) / changedValues
5
+ // 入力の読み取り: isColor / toNumber / toColor / toWidth / toLength / toDegree / toOption / toOptionNumber /
6
+ // toOptionNumberOrNull / toBoolean / toOptionalText
7
+ // graph 系の検査: checkGraph(data, prefix) / flattenNodes / checkUnique / findById
8
+ //
9
+ // 図ごとの知識(スキーマ・withDefaults・fieldsOf・図ごとの check・applyPatch)は各 model.* にある。
10
+ // 利用側(iHub やデモ)は各 model.* を読めばよく、このパッケージを直接 import する必要は無い
11
+ // (各 model.* が ModelError / mergePatch / getByPath などを再 export する)。
12
+ export { ModelError } from './error.js';
13
+ export { getByPath, mergePatch, put, movePatch, changedValues } from './patch.js';
14
+ export { toNumber, isColor, toColor, toWidth, toLength, toDegree, toOption, toOptionNumber, toOptionNumberOrNull, toBoolean, toOptionalText, } from './read.js';
15
+ export { flattenNodes, checkUnique, checkGraph, findById } from './graph.js';
@@ -0,0 +1,24 @@
1
+ import type { MergePatch, ModelChange, ModelField, ModelMove, ModelValues, Positioned } from './types.js';
2
+ /** ドット区切りの path で値を取り出す */
3
+ export declare function getByPath(obj: unknown, path: string): unknown;
4
+ /**
5
+ * JSON Merge Patch をレコードに重ねたものを返す(入力は書き換えない)。
6
+ * null の項目は除く。保存したあと、表示を先に更新するときに使う。
7
+ */
8
+ export declare function mergePatch<T extends object>(record: T, patch: MergePatch): T;
9
+ /** patch の入れ子に値を置く(put(patch, 'border.color', v) で patch.border.color = v) */
10
+ export declare function put(patch: MergePatch, path: string, value: unknown): void;
11
+ /**
12
+ * ドラッグで動かしたレコードの位置を保存する patch。
13
+ * 保存してある position に移動量を足し、整数に丸める(position が無いときは 0, 0 から)。x / y 以外(z など)は触らない。
14
+ * 入れ子のノードの position は親の中での位置なので、親を動かしても子の patch は要らない。
15
+ *
16
+ * @param collection 保存先(graph 系は 'nodes'、階層形式は 'entities')
17
+ */
18
+ export declare function movePatch<C extends string>(record: Positioned, move: ModelMove, collection: C): ModelChange<C>;
19
+ /**
20
+ * インスペクタの入力のうち、図に描いている値(effective = withDefaults で埋めたレコード)から変わった項目だけを返す。
21
+ * 前後の空白は除く。読み取り専用の項目と、values に無い項目は含めない。
22
+ */
23
+ export declare function changedValues(effective: object, values: ModelValues, fields: readonly ModelField[]): Map<string, string>;
24
+ //# sourceMappingURL=patch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"patch.d.ts","sourceRoot":"","sources":["../src/patch.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,UAAU,EAAE,SAAS,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE1G,2BAA2B;AAC3B,wBAAgB,SAAS,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAK7D;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,CAAC,SAAS,MAAM,EAAE,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,UAAU,GAAG,CAAC,CAmB5E;AAED,8EAA8E;AAC9E,wBAAgB,GAAG,CAAC,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAYzE;AAED;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,CAAC,SAAS,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,SAAS,EAAE,UAAU,EAAE,CAAC,GAAG,WAAW,CAAC,CAAC,CAAC,CAa9G;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,SAAS,UAAU,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAmBxH"}
package/dist/patch.js ADDED
@@ -0,0 +1,74 @@
1
+ // patch(JSON Merge Patch)を扱う道具。各 model.* の applyPatch / movePatch はこれを組み合わせて作る。
2
+ /** ドット区切りの path で値を取り出す */
3
+ export function getByPath(obj, path) {
4
+ return path.split('.').reduce((acc, key) => (acc == null ? acc : acc[key]), obj);
5
+ }
6
+ /**
7
+ * JSON Merge Patch をレコードに重ねたものを返す(入力は書き換えない)。
8
+ * null の項目は除く。保存したあと、表示を先に更新するときに使う。
9
+ */
10
+ export function mergePatch(record, patch) {
11
+ const out = { ...record };
12
+ for (const [key, value] of Object.entries(patch)) {
13
+ if (value === null) {
14
+ delete out[key];
15
+ }
16
+ else if (typeof value === 'object' && !Array.isArray(value)) {
17
+ const base = out[key];
18
+ out[key] = mergePatch(base && typeof base === 'object' && !Array.isArray(base) ? base : {}, value);
19
+ }
20
+ else {
21
+ out[key] = value;
22
+ }
23
+ }
24
+ return out;
25
+ }
26
+ /** patch の入れ子に値を置く(put(patch, 'border.color', v) で patch.border.color = v) */
27
+ export function put(patch, path, value) {
28
+ const keys = path.split('.');
29
+ let cur = patch;
30
+ for (const key of keys.slice(0, -1)) {
31
+ if (!cur[key] || typeof cur[key] !== 'object')
32
+ cur[key] = {};
33
+ cur = cur[key];
34
+ }
35
+ cur[keys[keys.length - 1]] = value;
36
+ }
37
+ /**
38
+ * ドラッグで動かしたレコードの位置を保存する patch。
39
+ * 保存してある position に移動量を足し、整数に丸める(position が無いときは 0, 0 から)。x / y 以外(z など)は触らない。
40
+ * 入れ子のノードの position は親の中での位置なので、親を動かしても子の patch は要らない。
41
+ *
42
+ * @param collection 保存先(graph 系は 'nodes'、階層形式は 'entities')
43
+ */
44
+ export function movePatch(record, move, collection) {
45
+ const base = record.position || { x: 0, y: 0 };
46
+ return {
47
+ collection,
48
+ id: record.id,
49
+ patch: {
50
+ position: {
51
+ x: Math.round(base.x + move.dx),
52
+ y: Math.round(base.y + move.dy),
53
+ },
54
+ },
55
+ };
56
+ }
57
+ /**
58
+ * インスペクタの入力のうち、図に描いている値(effective = withDefaults で埋めたレコード)から変わった項目だけを返す。
59
+ * 前後の空白は除く。読み取り専用の項目と、values に無い項目は含めない。
60
+ */
61
+ export function changedValues(effective, values, fields) {
62
+ const out = new Map();
63
+ for (const field of fields) {
64
+ if (field.type === 'readonly')
65
+ continue;
66
+ const value = values[field.path];
67
+ if (value === undefined)
68
+ continue;
69
+ const current = getByPath(effective, field.path);
70
+ if (value.trim() !== String(current ?? ''))
71
+ out.set(field.path, value.trim());
72
+ }
73
+ return out;
74
+ }