@yanqirenshi/model.tm 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.
@@ -0,0 +1,133 @@
1
+ // 「書いていない項目」に view(d3.ter)が既定で使う値と、レコードの種類の判別。
2
+ //
3
+ // 値の出どころ: d3.ter の src/js/Entity.js(entityTypeContents = 種別バッジ、entityBackground = 配色)、
4
+ // Port.js(degree() の既定 0)。d3.ter はこのファイルを読む(#144)ので、ここが正。
5
+ // cardinality / optionality に既定は置かない(d3.ter は無ければ記号を描かない)。
6
+ /** エンティティの種別(10 種) */
7
+ export const ENTITY_TYPES = [
8
+ 'RESOURCE', 'RESOURCE-SUBSET', 'EVENT', 'EVENT-SUBSET',
9
+ 'COMPARATIVE', 'CORRESPONDENCE', 'RECURSION', 'MANY-VALUED-OR', 'MANY-VALUED-AND', 'CLASS',
10
+ ];
11
+ /** 種別バッジ(箱のヘッダ右に描く文字。CLASS は意図的に無し = '') */
12
+ export const TYPE_BADGES = {
13
+ 'RESOURCE': 'Rsc',
14
+ 'RESOURCE-SUBSET': 'Rsc',
15
+ 'EVENT': 'Evt',
16
+ 'EVENT-SUBSET': 'Evt',
17
+ 'COMPARATIVE': '対照',
18
+ 'CORRESPONDENCE': '対応',
19
+ 'RECURSION': '再帰',
20
+ 'MANY-VALUED-OR': 'MO',
21
+ 'MANY-VALUED-AND': 'MA',
22
+ 'CLASS': '',
23
+ };
24
+ /** 種別ごとの箱の背景色 */
25
+ export const TYPE_COLORS = {
26
+ 'RESOURCE': '#89c3eb',
27
+ 'RESOURCE-SUBSET': '#a0d8ef',
28
+ 'EVENT': '#f09199',
29
+ 'EVENT-SUBSET': '#f6bfbc',
30
+ 'COMPARATIVE': '#c8d5bb',
31
+ 'CORRESPONDENCE': '#f2f2b0',
32
+ 'RECURSION': '#dbd0e6',
33
+ 'MANY-VALUED-OR': '#fddea5',
34
+ 'MANY-VALUED-AND': '#9fd5c9',
35
+ 'CLASS': '#bdc0ba',
36
+ };
37
+ /** 結線の中点から垂下(mapping)できる種別 */
38
+ export const MAPPING_TYPES = ['CORRESPONDENCE', 'COMPARATIVE', 'RECURSION'];
39
+ /** サブセット結線の区分コードの種類 */
40
+ export const SUBSET_KINDS = ['same', 'different'];
41
+ /** カーディナリティ(1 か 多) */
42
+ export const CARDINALITIES = [1, 3];
43
+ /** オプショナリティ(0 は任意、1 は必須) */
44
+ export const OPTIONALITIES = [0, 1];
45
+ /** エンティティの既定。size の 0 は「内容に合わせる」 */
46
+ export const ENTITY_DEFAULTS = {
47
+ position: { x: 0, y: 0 },
48
+ size: { w: 0, h: 0 },
49
+ };
50
+ /** 関係の既定。端点の角度は 0(真下)。cardinality / optionality は既定なし(無ければ記号を描かない) */
51
+ export const RELATIONSHIP_DEFAULTS = {
52
+ end: { position: 0 },
53
+ };
54
+ /** 種別として正しいか */
55
+ export function isEntityType(value) {
56
+ return typeof value === 'string' && ENTITY_TYPES.includes(value);
57
+ }
58
+ /** 関係か(from と to がエンティティを指す) */
59
+ export function isRelationship(record) {
60
+ const r = record;
61
+ return !!(r && r.from && typeof r.from === 'object' && 'entity' in r.from
62
+ && r.to && typeof r.to === 'object' && 'entity' in r.to);
63
+ }
64
+ /**
65
+ * エンティティか。関係でなく、type・インスタンスの配列・位置・大きさのどれかを持つ。
66
+ * マスタ(id と name だけ)と区別するための判定なので、id と name しか無いエンティティは見分けられない
67
+ * (entities[] の要素と分かっているときは entityWithDefaults を使う)。
68
+ */
69
+ export function isEntity(record) {
70
+ if (!record || isRelationship(record))
71
+ return false;
72
+ const e = record;
73
+ return typeof e.type === 'string' || Array.isArray(e.identifiers) || Array.isArray(e.attributes)
74
+ || e.position !== undefined || e.size !== undefined;
75
+ }
76
+ /** レコードの種類 */
77
+ export function kindOf(record) {
78
+ if (isRelationship(record))
79
+ return 'relationship';
80
+ return isEntity(record) ? 'entity' : 'master';
81
+ }
82
+ /** 表示する名前。文字列ならそのまま、Name なら logical(無ければ physical)。無ければ '' */
83
+ export function nameOf(record) {
84
+ const name = record.name;
85
+ if (typeof name === 'string')
86
+ return name;
87
+ if (name && typeof name === 'object')
88
+ return name.logical || name.physical || '';
89
+ return '';
90
+ }
91
+ /** 端点に既定を重ねる(角度が無ければ 0。cardinality / optionality は不正値なら除く) */
92
+ export function endWithDefaults(end) {
93
+ const out = {
94
+ ...end,
95
+ position: typeof end.position === 'number' && Number.isFinite(end.position) ? end.position : RELATIONSHIP_DEFAULTS.end.position,
96
+ };
97
+ if (!CARDINALITIES.includes(end.cardinality))
98
+ delete out.cardinality;
99
+ if (!OPTIONALITIES.includes(end.optionality))
100
+ delete out.optionality;
101
+ return out;
102
+ }
103
+ /** 関係に既定を重ねる: from / to の position */
104
+ export function relationshipWithDefaults(record) {
105
+ return {
106
+ ...record,
107
+ from: endWithDefaults(record.from),
108
+ to: endWithDefaults(record.to),
109
+ };
110
+ }
111
+ /**
112
+ * エンティティに既定を重ねる: position(無ければ 0, 0)、size(無ければ 0, 0 = 内容に合わせる)。
113
+ * type は補わない(無ければ無いまま。d3.ter は描けないので check で知らせる)。
114
+ */
115
+ export function entityWithDefaults(record) {
116
+ return {
117
+ ...record,
118
+ position: { ...ENTITY_DEFAULTS.position, ...(record.position || {}) },
119
+ size: { ...ENTITY_DEFAULTS.size, ...(record.size || {}) },
120
+ };
121
+ }
122
+ /**
123
+ * レコードに、view が既定で使う値を重ねて返す(入力は書き換えない)。インスペクタの初期値に使う。
124
+ * 形から種類を決める(kindOf)。マスタはそのまま(複製だけ)。
125
+ * name は文字列か Name かを保つ(どちらで書いたかで編集できる項目が変わる — fieldsOf)。
126
+ */
127
+ export function withDefaults(record) {
128
+ if (isRelationship(record))
129
+ return relationshipWithDefaults(record);
130
+ if (isEntity(record))
131
+ return entityWithDefaults(record);
132
+ return { ...record };
133
+ }
@@ -0,0 +1,19 @@
1
+ import type { TmCollection, TmField, TmRecord } from './types.js';
2
+ /** 名前の項目。name が Name(オブジェクト)なら論理名・物理名の 2 項目、それ以外は名称 1 項目 */
3
+ export declare function nameFields(record: TmRecord): TmField[];
4
+ /** エンティティの項目(名前のあと) */
5
+ export declare const ENTITY_FIELDS: readonly TmField[];
6
+ /**
7
+ * 関係の項目。接続位置はエンティティの中心から見た角度(0=真下・時計回り。90=左 / 180=真上 / 270=右)。
8
+ * カーディナリティ・オプショナリティは空にできる(記号を描かない)。
9
+ */
10
+ export declare const RELATIONSHIP_FIELDS: readonly TmField[];
11
+ /** マスタ(識別子・属性)の項目(名前のあと) */
12
+ export declare const MASTER_FIELDS: readonly TmField[];
13
+ /**
14
+ * レコードに応じた項目の一覧。
15
+ * マスタは identifiers / attributes のどちらかをレコードの形から判別できないので、利用側が collection を渡す
16
+ * (項目は同じなので、渡さなくても一覧は変わらない。applyPatch では必須)。
17
+ */
18
+ export declare function fieldsOf(record: TmRecord, _collection?: TmCollection): TmField[];
19
+ //# sourceMappingURL=fields.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fields.d.ts","sourceRoot":"","sources":["../src/fields.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAKlE,4DAA4D;AAC5D,wBAAgB,UAAU,CAAC,MAAM,EAAE,QAAQ,GAAG,OAAO,EAAE,CAUtD;AAED,uBAAuB;AACvB,eAAO,MAAM,aAAa,EAAE,SAAS,OAAO,EAM3C,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,mBAAmB,EAAE,SAAS,OAAO,EAgBjD,CAAC;AAEF,4BAA4B;AAC5B,eAAO,MAAM,aAAa,EAAE,SAAS,OAAO,EAG3C,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,MAAM,EAAE,QAAQ,EAAE,WAAW,CAAC,EAAE,YAAY,GAAG,OAAO,EAAE,CAShF"}
package/dist/fields.js ADDED
@@ -0,0 +1,72 @@
1
+ // 編集できる項目の一覧。インスペクタ(colonoscope の ModelInspector)に渡す。
2
+ //
3
+ // path はレコードの実際の項目を指す。既定値は withDefaults() で重ねてから渡すので、
4
+ // 書いていない項目(関係の角度など)も、図に描かれている値が初期値として出る。
5
+ // type: 'readonly' は表示だけ。options は取りうる値の一覧(検証にも使う。文字列で持つ)。
6
+ //
7
+ // 名前は、レコードが文字列で持っていれば name、{ logical, physical } で持っていれば
8
+ // name.logical / name.physical を出す(どちらで書いたかを変えない)。
9
+ // エンティティのインスタンス(識別子・属性の行)は entities[] の入れ子で、JSON Merge Patch では 1 要素を指せないので
10
+ // 項目にしない(行を右クリックしたときはマスタを開く)。
11
+ import { CARDINALITIES, ENTITY_TYPES, OPTIONALITIES, SUBSET_KINDS, kindOf } from './defaults.js';
12
+ const CARDINALITY_OPTIONS = CARDINALITIES.map(String);
13
+ const OPTIONALITY_OPTIONS = OPTIONALITIES.map(String);
14
+ /** 名前の項目。name が Name(オブジェクト)なら論理名・物理名の 2 項目、それ以外は名称 1 項目 */
15
+ export function nameFields(record) {
16
+ const name = record.name;
17
+ if (name && typeof name === 'object')
18
+ return [
19
+ { path: 'name.logical', label: '論理名', type: 'text' },
20
+ { path: 'name.physical', label: '物理名', type: 'text' },
21
+ ];
22
+ return [{ path: 'name', label: '名称', type: 'text' }];
23
+ }
24
+ /** エンティティの項目(名前のあと) */
25
+ export const ENTITY_FIELDS = [
26
+ { path: 'type', label: '種別', type: 'text', options: ENTITY_TYPES },
27
+ { path: 'id', label: 'ID', type: 'readonly' },
28
+ { path: 'description', label: '説明', type: 'textarea' },
29
+ { path: 'size.w', label: '幅 (0 で自動)', type: 'number' },
30
+ { path: 'size.h', label: '高さ (0 で自動)', type: 'number' },
31
+ ];
32
+ /**
33
+ * 関係の項目。接続位置はエンティティの中心から見た角度(0=真下・時計回り。90=左 / 180=真上 / 270=右)。
34
+ * カーディナリティ・オプショナリティは空にできる(記号を描かない)。
35
+ */
36
+ export const RELATIONSHIP_FIELDS = [
37
+ { path: 'id', label: 'ID', type: 'readonly' },
38
+ { path: 'type', label: '種類', type: 'text' },
39
+ { path: 'label', label: 'ラベル', type: 'text' },
40
+ { path: 'from.entity', label: '接続元のエンティティ', type: 'readonly' },
41
+ { path: 'from.position', label: '接続元の位置 (角度)', type: 'number' },
42
+ { path: 'from.cardinality', label: '接続元のカーディナリティ (空で無し)', type: 'text', options: CARDINALITY_OPTIONS },
43
+ { path: 'from.optionality', label: '接続元のオプショナリティ (空で無し)', type: 'text', options: OPTIONALITY_OPTIONS },
44
+ { path: 'to.entity', label: '接続先のエンティティ', type: 'readonly' },
45
+ { path: 'to.position', label: '接続先の位置 (角度)', type: 'number' },
46
+ { path: 'to.cardinality', label: '接続先のカーディナリティ (空で無し)', type: 'text', options: CARDINALITY_OPTIONS },
47
+ { path: 'to.optionality', label: '接続先のオプショナリティ (空で無し)', type: 'text', options: OPTIONALITY_OPTIONS },
48
+ { path: 'subset.kind', label: 'サブセットの区分 (same は =、different は ×)', type: 'text', options: SUBSET_KINDS },
49
+ { path: 'subset.code', label: 'サブセットの区分コード', type: 'text' },
50
+ { path: 'mapping.entity', label: '垂下する表 (エンティティ)', type: 'readonly' },
51
+ { path: 'description', label: '説明', type: 'textarea' },
52
+ ];
53
+ /** マスタ(識別子・属性)の項目(名前のあと) */
54
+ export const MASTER_FIELDS = [
55
+ { path: 'description', label: '説明', type: 'textarea' },
56
+ { path: 'id', label: 'ID', type: 'readonly' },
57
+ ];
58
+ /**
59
+ * レコードに応じた項目の一覧。
60
+ * マスタは identifiers / attributes のどちらかをレコードの形から判別できないので、利用側が collection を渡す
61
+ * (項目は同じなので、渡さなくても一覧は変わらない。applyPatch では必須)。
62
+ */
63
+ export function fieldsOf(record, _collection) {
64
+ switch (kindOf(record)) {
65
+ case 'relationship':
66
+ return [...RELATIONSHIP_FIELDS];
67
+ case 'entity':
68
+ return [...nameFields(record), ...ENTITY_FIELDS];
69
+ default:
70
+ return [...nameFields(record), ...MASTER_FIELDS];
71
+ }
72
+ }
@@ -0,0 +1,8 @@
1
+ export type { TmId, TmName, TmEntityType, TmMasterRecord, TmIdentifierRef, TmAttributeRef, TmEntityRecord, TmCardinality, TmOptionality, TmRelationshipEnd, TmRelationshipDetail, TmSubset, TmRelationshipRecord, TmRecord, TmGraph, TmMasterCollection, TmCollection, TmKind, MergePatch, TmChange, TmMove, TmFieldType, TmField, TmValues, } from './types.js';
2
+ export type { TmFilledEnd } from './defaults.js';
3
+ export { ENTITY_TYPES, TYPE_BADGES, TYPE_COLORS, MAPPING_TYPES, SUBSET_KINDS, CARDINALITIES, OPTIONALITIES, ENTITY_DEFAULTS, RELATIONSHIP_DEFAULTS, isEntityType, isRelationship, isEntity, kindOf, nameOf, endWithDefaults, entityWithDefaults, relationshipWithDefaults, withDefaults, } from './defaults.js';
4
+ export { SCHEMAS, IDENTIFIERS_SCHEMA, ATTRIBUTES_SCHEMA, ENTITIES_SCHEMA, RELATIONSHIPS_SCHEMA } from './schema.js';
5
+ export { check, findById } from './check.js';
6
+ export { ENTITY_FIELDS, RELATIONSHIP_FIELDS, MASTER_FIELDS, nameFields, fieldsOf } from './fields.js';
7
+ export { ModelError, collectionOf, titleOf, getByPath, mergePatch, movePatch, applyPatch } from './patch.js';
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAWA,YAAY,EACV,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,cAAc,EAAE,eAAe,EAAE,cAAc,EAAE,cAAc,EAC3F,aAAa,EAAE,aAAa,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,QAAQ,EAAE,oBAAoB,EACrG,QAAQ,EAAE,OAAO,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,EAC3D,UAAU,EAAE,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,OAAO,EAAE,QAAQ,GAC7D,MAAM,YAAY,CAAC;AAEpB,YAAY,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAEjD,OAAO,EACL,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,aAAa,EAAE,YAAY,EAAE,aAAa,EAAE,aAAa,EACjG,eAAe,EAAE,qBAAqB,EACtC,YAAY,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,EAAE,kBAAkB,EAAE,wBAAwB,EAAE,YAAY,GACpI,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAE,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,eAAe,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACpH,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,mBAAmB,EAAE,aAAa,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACtG,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,15 @@
1
+ // model.tm — TM(T字形ER。d3.ter)の「図ごとの知識」。描画はしない(react / d3 に依存しない)。
2
+ //
3
+ // スキーマ: SCHEMAS / *_SCHEMA(JSON Schema 2020-12。4 コレクション)
4
+ // 整合性ルール: check(data)(id の重複・参照先の存在・CLASS と mapping の結線の規則)
5
+ // 既定値の補完: withDefaults(record) / entityWithDefaults / relationshipWithDefaults / kindOf / nameOf
6
+ // 種別の表示: ENTITY_TYPES / TYPE_BADGES / TYPE_COLORS(d3.ter が読む)
7
+ // 編集できる項目: fieldsOf(record, collection?) / nameFields / ENTITY_FIELDS / RELATIONSHIP_FIELDS / MASTER_FIELDS
8
+ // patch への変換: movePatch(entity, move) / applyPatch(record, values, collection?) / mergePatch(record, patch)
9
+ //
10
+ // view(d3.ter)はこのパッケージの既定値(種別バッジ・配色・角度)を読む。インスペクタ(colonoscope)との間に横の依存は無い。
11
+ export { ENTITY_TYPES, TYPE_BADGES, TYPE_COLORS, MAPPING_TYPES, SUBSET_KINDS, CARDINALITIES, OPTIONALITIES, ENTITY_DEFAULTS, RELATIONSHIP_DEFAULTS, isEntityType, isRelationship, isEntity, kindOf, nameOf, endWithDefaults, entityWithDefaults, relationshipWithDefaults, withDefaults, } from './defaults.js';
12
+ export { SCHEMAS, IDENTIFIERS_SCHEMA, ATTRIBUTES_SCHEMA, ENTITIES_SCHEMA, RELATIONSHIPS_SCHEMA } from './schema.js';
13
+ export { check, findById } from './check.js';
14
+ export { ENTITY_FIELDS, RELATIONSHIP_FIELDS, MASTER_FIELDS, nameFields, fieldsOf } from './fields.js';
15
+ export { ModelError, collectionOf, titleOf, getByPath, mergePatch, movePatch, applyPatch } from './patch.js';
@@ -0,0 +1,31 @@
1
+ import type { MergePatch, TmChange, TmCollection, TmEntityRecord, TmMove, TmRecord, TmValues } from './types.js';
2
+ /** 入力の検証エラー。message は利用者向け */
3
+ export declare class ModelError extends Error {
4
+ constructor(message: string);
5
+ }
6
+ /**
7
+ * レコードを置くコレクション。関係・エンティティは形から決まる。
8
+ * マスタは identifiers / attributes を形から判別できないので collection が要る。
9
+ */
10
+ export declare function collectionOf(record: TmRecord, collection?: TmCollection): TmCollection;
11
+ /** インスペクタの見出し。名前、無ければ id(関係はラベル、無ければ「種類 id」) */
12
+ export declare function titleOf(record: TmRecord): string;
13
+ /** ドット区切りの path で値を取り出す */
14
+ export declare function getByPath(obj: unknown, path: string): unknown;
15
+ /**
16
+ * JSON Merge Patch をレコードに重ねたものを返す(入力は書き換えない)。
17
+ * null の項目は除く。保存したあと、表示を先に更新するときに使う。
18
+ */
19
+ export declare function mergePatch<T extends object>(record: T, patch: MergePatch): T;
20
+ /**
21
+ * ドラッグで動かしたエンティティの位置を保存する patch。
22
+ * 保存してある position に移動量を足し、整数に丸める(position が無いときは 0, 0 から)。z は触らない。
23
+ */
24
+ export declare function movePatch(entity: TmEntityRecord, move: TmMove): TmChange;
25
+ /**
26
+ * インスペクタの「適用」の値を、保存する patch に変換する。
27
+ * 図に描いている値から変えた項目だけを patch に入れる(変えていない既定の値まで書き込まないため)。
28
+ * 変更が無ければ patch は {}。マスタのときは collection(identifiers / attributes)が要る。
29
+ */
30
+ export declare function applyPatch(record: TmRecord, values: TmValues, collection?: TmCollection): TmChange;
31
+ //# sourceMappingURL=patch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"patch.d.ts","sourceRoot":"","sources":["../src/patch.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EACV,UAAU,EAAE,QAAQ,EAAE,YAAY,EAAE,cAAc,EAAkB,MAAM,EAAE,QAAQ,EAAwB,QAAQ,EACrH,MAAM,YAAY,CAAC;AAEpB,8BAA8B;AAC9B,qBAAa,UAAW,SAAQ,KAAK;gBACvB,OAAO,EAAE,MAAM;CAI5B;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,QAAQ,EAAE,UAAU,CAAC,EAAE,YAAY,GAAG,YAAY,CAWtF;AAED,gDAAgD;AAChD,wBAAgB,OAAO,CAAC,MAAM,EAAE,QAAQ,GAAG,MAAM,CAWhD;AAED,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;;;GAGG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,GAAG,QAAQ,CAaxE;AA2ND;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,UAAU,CAAC,EAAE,YAAY,GAAG,QAAQ,CAUlG"}
package/dist/patch.js ADDED
@@ -0,0 +1,264 @@
1
+ // UI の操作(ドラッグ・インスペクタの適用)を、保存する patch(JSON Merge Patch)に変換する。
2
+ //
3
+ // 戻り値は { collection, id, patch }。利用側(iHub など)は図の種類を知らずにこれを保存すればよい。
4
+ // 入力の検証に失敗したときは ModelError を投げる(メッセージは利用者にそのまま見せられる日本語)。
5
+ // 使い方の誤り(マスタなのに collection を渡していない)は ModelError ではなく Error。
6
+ import { CARDINALITIES, ENTITY_TYPES, OPTIONALITIES, SUBSET_KINDS, isEntity, isRelationship, kindOf, nameOf, withDefaults, } from './defaults.js';
7
+ import { fieldsOf } from './fields.js';
8
+ /** 入力の検証エラー。message は利用者向け */
9
+ export class ModelError extends Error {
10
+ constructor(message) {
11
+ super(message);
12
+ this.name = 'ModelError';
13
+ }
14
+ }
15
+ /**
16
+ * レコードを置くコレクション。関係・エンティティは形から決まる。
17
+ * マスタは identifiers / attributes を形から判別できないので collection が要る。
18
+ */
19
+ export function collectionOf(record, collection) {
20
+ if (isRelationship(record))
21
+ return 'relationships';
22
+ if (isEntity(record))
23
+ return 'entities';
24
+ if (!collection)
25
+ throw new Error('model.tm: マスタ (識別子・属性) のレコードには collection を渡してください。');
26
+ return collection;
27
+ }
28
+ /** インスペクタの見出し。名前、無ければ id(関係はラベル、無ければ「種類 id」) */
29
+ export function titleOf(record) {
30
+ if (isRelationship(record)) {
31
+ if (typeof record.label === 'string' && record.label !== '')
32
+ return record.label;
33
+ return `${record.type || ''} ${String(record.id)}`.trim();
34
+ }
35
+ const name = nameOf(record);
36
+ return name !== '' ? name : String(record.id);
37
+ }
38
+ /** ドット区切りの path で値を取り出す */
39
+ export function getByPath(obj, path) {
40
+ return path.split('.').reduce((acc, key) => (acc == null ? acc : acc[key]), obj);
41
+ }
42
+ /**
43
+ * JSON Merge Patch をレコードに重ねたものを返す(入力は書き換えない)。
44
+ * null の項目は除く。保存したあと、表示を先に更新するときに使う。
45
+ */
46
+ export function mergePatch(record, patch) {
47
+ const out = { ...record };
48
+ for (const [key, value] of Object.entries(patch)) {
49
+ if (value === null) {
50
+ delete out[key];
51
+ }
52
+ else if (typeof value === 'object' && !Array.isArray(value)) {
53
+ const base = out[key];
54
+ out[key] = mergePatch(base && typeof base === 'object' && !Array.isArray(base) ? base : {}, value);
55
+ }
56
+ else {
57
+ out[key] = value;
58
+ }
59
+ }
60
+ return out;
61
+ }
62
+ /**
63
+ * ドラッグで動かしたエンティティの位置を保存する patch。
64
+ * 保存してある position に移動量を足し、整数に丸める(position が無いときは 0, 0 から)。z は触らない。
65
+ */
66
+ export function movePatch(entity, move) {
67
+ const base = entity.position || { x: 0, y: 0 };
68
+ return {
69
+ collection: 'entities',
70
+ id: entity.id,
71
+ patch: {
72
+ position: {
73
+ x: Math.round(base.x + move.dx),
74
+ y: Math.round(base.y + move.dy),
75
+ },
76
+ },
77
+ };
78
+ }
79
+ // ---- 入力の読み取り ------------------------------------------------------------
80
+ // 入力した文字列を数にする。数でないときは null。
81
+ function toNumber(value) {
82
+ const n = Number(value);
83
+ return value !== '' && Number.isFinite(n) ? n : null;
84
+ }
85
+ // 0 以上の整数(幅・高さ。0 は「内容に合わせる」)
86
+ function toLength(value, label) {
87
+ const n = toNumber(value);
88
+ if (n === null || n < 0)
89
+ throw new ModelError(`${label}は 0 以上の数で入力してください。(0 で内容に合わせる)`);
90
+ return Math.round(n);
91
+ }
92
+ // 角度(0 以上 360 未満の整数に正規化)
93
+ function toDegree(value, label) {
94
+ const n = toNumber(value);
95
+ if (n === null)
96
+ throw new ModelError(`${label}は、角度 (数) で入力してください。`);
97
+ return ((Math.round(n) % 360) + 360) % 360;
98
+ }
99
+ // 候補の中の数、または空(null = 項目を除く)
100
+ function toOptionNumberOrNull(value, options, label) {
101
+ if (value === '')
102
+ return null;
103
+ const n = toNumber(value);
104
+ if (n === null || !options.includes(n))
105
+ throw new ModelError(`${label}は、${options.join(' / ')} のどれか (または空) で入力してください。`);
106
+ return n;
107
+ }
108
+ // 候補の中の文字列
109
+ function toOption(value, options, label) {
110
+ if (!options.includes(value))
111
+ throw new ModelError(`${label}は、${options.join(' / ')} のどれかで入力してください。`);
112
+ return value;
113
+ }
114
+ // 空にできる文字列。空なら null(JSON Merge Patch で項目を除く)
115
+ function toOptionalText(value) {
116
+ return value === '' ? null : value;
117
+ }
118
+ // patch の入れ子に値を置く(patch.from.position = v のように)
119
+ function put(patch, path, value) {
120
+ const keys = path.split('.');
121
+ let cur = patch;
122
+ for (const key of keys.slice(0, -1)) {
123
+ if (!cur[key] || typeof cur[key] !== 'object')
124
+ cur[key] = {};
125
+ cur = cur[key];
126
+ }
127
+ cur[keys[keys.length - 1]] = value;
128
+ }
129
+ /**
130
+ * インスペクタの入力のうち、図に描いている値(withDefaults で埋めた値)から変わった項目だけを返す。
131
+ * 前後の空白は除く。読み取り専用の項目と、values に無い項目は含めない。
132
+ */
133
+ function changedValues(record, values, collection) {
134
+ const effective = withDefaults(record);
135
+ const out = new Map();
136
+ for (const field of fieldsOf(record, collection)) {
137
+ if (field.type === 'readonly')
138
+ continue;
139
+ const value = values[field.path];
140
+ if (value === undefined)
141
+ continue;
142
+ const current = getByPath(effective, field.path);
143
+ if (value.trim() !== String(current ?? ''))
144
+ out.set(field.path, value.trim());
145
+ }
146
+ return out;
147
+ }
148
+ // 名前(name / name.logical / name.physical)。required なら空にできない
149
+ function namePatch(patch, path, value, required) {
150
+ // name と name.logical は空にできない。name.physical は空にできる(論理名だけのモデル)
151
+ if (value === '' && required)
152
+ throw new ModelError('名称を入力してください。');
153
+ put(patch, path, value);
154
+ }
155
+ // エンティティの入力 → patch(name / type / description / size)
156
+ function entityPatch(entity, values) {
157
+ const patch = {};
158
+ for (const [path, value] of changedValues(entity, values)) {
159
+ switch (path) {
160
+ case 'name':
161
+ case 'name.logical':
162
+ namePatch(patch, path, value, true);
163
+ break;
164
+ case 'name.physical':
165
+ namePatch(patch, path, value, false);
166
+ break;
167
+ case 'type':
168
+ put(patch, path, toOption(value, ENTITY_TYPES, '種別'));
169
+ break;
170
+ case 'size.w':
171
+ put(patch, path, toLength(value, '幅'));
172
+ break;
173
+ case 'size.h':
174
+ put(patch, path, toLength(value, '高さ'));
175
+ break;
176
+ case 'description':
177
+ put(patch, path, toOptionalText(value));
178
+ break;
179
+ }
180
+ }
181
+ return patch;
182
+ }
183
+ const END_LABELS = { from: '接続元', to: '接続先' };
184
+ // 関係の入力 → patch(type / label / from / to / subset / description)
185
+ function relationshipPatch(relationship, values) {
186
+ const patch = {};
187
+ for (const [path, value] of changedValues(relationship, values)) {
188
+ const [head, key] = path.split('.');
189
+ if ((head === 'from' || head === 'to') && key) {
190
+ const label = END_LABELS[head];
191
+ switch (key) {
192
+ case 'position':
193
+ put(patch, path, toDegree(value, `${label}の位置`));
194
+ break;
195
+ case 'cardinality':
196
+ put(patch, path, toOptionNumberOrNull(value, CARDINALITIES, `${label}のカーディナリティ`));
197
+ break;
198
+ case 'optionality':
199
+ put(patch, path, toOptionNumberOrNull(value, OPTIONALITIES, `${label}のオプショナリティ`));
200
+ break;
201
+ }
202
+ continue;
203
+ }
204
+ switch (path) {
205
+ case 'subset.kind':
206
+ put(patch, path, toOption(value, SUBSET_KINDS, 'サブセットの区分'));
207
+ break;
208
+ case 'subset.code':
209
+ // 区分コードを空にするとサブセットの表記を消す(subset ごと除く)
210
+ if (value === '')
211
+ patch.subset = null;
212
+ else
213
+ put(patch, path, value);
214
+ break;
215
+ case 'type':
216
+ case 'label':
217
+ case 'description':
218
+ put(patch, path, toOptionalText(value));
219
+ break;
220
+ }
221
+ }
222
+ // subset を除くときは kind の変更も要らない
223
+ if (patch.subset === null)
224
+ return patch;
225
+ // kind だけを新しく書いて code が無い subset は作らない(d3.ter は code が無ければ描かない)
226
+ const subset = patch.subset;
227
+ if (subset && subset.kind !== undefined && subset.code === undefined && !relationship.subset?.code)
228
+ throw new ModelError('サブセットの区分コードを入力してください。');
229
+ return patch;
230
+ }
231
+ // マスタの入力 → patch(name / description)
232
+ function masterPatch(master, values, collection) {
233
+ const patch = {};
234
+ for (const [path, value] of changedValues(master, values, collection)) {
235
+ switch (path) {
236
+ case 'name':
237
+ case 'name.logical':
238
+ // 識別子・属性の名前は空にできない(エンティティの行に描く文字が無くなる)
239
+ namePatch(patch, path, value, true);
240
+ break;
241
+ case 'name.physical':
242
+ namePatch(patch, path, value, false);
243
+ break;
244
+ case 'description':
245
+ put(patch, path, toOptionalText(value));
246
+ break;
247
+ }
248
+ }
249
+ return patch;
250
+ }
251
+ /**
252
+ * インスペクタの「適用」の値を、保存する patch に変換する。
253
+ * 図に描いている値から変えた項目だけを patch に入れる(変えていない既定の値まで書き込まないため)。
254
+ * 変更が無ければ patch は {}。マスタのときは collection(identifiers / attributes)が要る。
255
+ */
256
+ export function applyPatch(record, values, collection) {
257
+ const target = collectionOf(record, collection);
258
+ const patch = kindOf(record) === 'relationship'
259
+ ? relationshipPatch(record, values)
260
+ : kindOf(record) === 'entity'
261
+ ? entityPatch(record, values)
262
+ : masterPatch(record, values, target);
263
+ return { collection: target, id: record.id, patch };
264
+ }