fieldsmith 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.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +843 -0
  3. package/assets/fonts/DotGothic16.ttf +0 -0
  4. package/assets/fonts/OFL.txt +93 -0
  5. package/dist/cli/exit.js +37 -0
  6. package/dist/cli/exit.js.map +1 -0
  7. package/dist/cli/index.js +699 -0
  8. package/dist/cli/index.js.map +1 -0
  9. package/dist/cli/output.js +61 -0
  10. package/dist/cli/output.js.map +1 -0
  11. package/dist/cli/progress.js +83 -0
  12. package/dist/cli/progress.js.map +1 -0
  13. package/dist/cli/promptInput.js +57 -0
  14. package/dist/cli/promptInput.js.map +1 -0
  15. package/dist/cli/schema.js +140 -0
  16. package/dist/cli/schema.js.map +1 -0
  17. package/dist/config.js +149 -0
  18. package/dist/config.js.map +1 -0
  19. package/dist/icon/emojiFont.js +163 -0
  20. package/dist/icon/emojiFont.js.map +1 -0
  21. package/dist/icon/render.js +136 -0
  22. package/dist/icon/render.js.map +1 -0
  23. package/dist/kintone/client.js +157 -0
  24. package/dist/kintone/client.js.map +1 -0
  25. package/dist/kintone/deploy.js +519 -0
  26. package/dist/kintone/deploy.js.map +1 -0
  27. package/dist/kintone/oauth.js +156 -0
  28. package/dist/kintone/oauth.js.map +1 -0
  29. package/dist/kintone/tokenStore.js +87 -0
  30. package/dist/kintone/tokenStore.js.map +1 -0
  31. package/dist/llm/generate.js +317 -0
  32. package/dist/llm/generate.js.map +1 -0
  33. package/dist/llm/prompt.js +142 -0
  34. package/dist/llm/prompt.js.map +1 -0
  35. package/dist/spec/appSpec.js +283 -0
  36. package/dist/spec/appSpec.js.map +1 -0
  37. package/dist/spec/diff.js +218 -0
  38. package/dist/spec/diff.js.map +1 -0
  39. package/dist/spec/fieldSpec.js +353 -0
  40. package/dist/spec/fieldSpec.js.map +1 -0
  41. package/dist/spec/fromKintone.js +230 -0
  42. package/dist/spec/fromKintone.js.map +1 -0
  43. package/dist/spec/layout.js +215 -0
  44. package/dist/spec/layout.js.map +1 -0
  45. package/dist/spec/toKintone.js +170 -0
  46. package/dist/spec/toKintone.js.map +1 -0
  47. package/package.json +59 -0
@@ -0,0 +1,142 @@
1
+ import { APP_THEMES } from "../spec/appSpec.js";
2
+ import { SUPPORTED_FIELD_TYPES, UNADDABLE_FIELD_TYPES } from "../spec/fieldSpec.js";
3
+ /**
4
+ * AppSpec を生成させるためのシステムプロンプト。
5
+ *
6
+ * 生成結果は必ず AppSpec スキーマで再検証するので、ここでの指示は
7
+ * 「検証を通す確率を上げるためのもの」であって、安全性の担保ではない。
8
+ * 特に kintone 固有の落とし穴 (選択肢の書き方、追加できないフィールド型、
9
+ * 計算式がフィールドコードを参照すること) を明示しておく。
10
+ */
11
+ export const SYSTEM_PROMPT = `あなたは kintone のアプリ設計に詳しいアシスタントです。
12
+ 日本語で書かれた業務要件を受け取り、kintone アプリの設計 (AppSpec) を JSON で出力してください。
13
+
14
+ ## 出力の方針
15
+
16
+ - 要件に書かれた項目を漏れなくフィールドにする。書かれていない項目を推測で大量に足さない。
17
+ - ただし、業務上ほぼ確実に必要になる項目 (ステータス的な区分、担当者、日付) が要件から明らかに読み取れる場合は補ってよい。
18
+ - ラベルは業務で使われる日本語をそのまま使う。英語に訳さない。
19
+ - フィールドの並び順は、記入する人が上から順に埋めていける自然な順序にする。
20
+ 同じ group のフィールドは必ず隣り合わせにする。
21
+
22
+ ## フィールド型の選び方
23
+
24
+ 利用できる型: ${SUPPORTED_FIELD_TYPES.join(", ")}
25
+
26
+ - 短いテキスト (名称、件名、コード) → SINGLE_LINE_TEXT
27
+ - 長文 (備考、内容) → MULTI_LINE_TEXT
28
+ - 金額・数量・件数 → NUMBER。金額なら digit: true と unit を設定する。
29
+ - 選択肢が 2〜4 個で必ず 1 つ選ぶ → RADIO_BUTTON、5 個以上または省略可 → DROP_DOWN
30
+ - 複数選べる → CHECK_BOX (選択肢が少ない) / MULTI_SELECT (選択肢が多い)
31
+ - 日付のみ → DATE、日時 → DATETIME、時刻のみ → TIME
32
+ - 社内の人を指す (担当者、承認者) → USER_SELECT
33
+ - URL・メールアドレス・電話番号 → LINK (protocol を WEB / MAIL / CALL から選ぶ)
34
+ - 他のフィールドから計算する値 → CALC。expression にはフィールドコードを使った式を書く (例: "単価 * 数量")。
35
+
36
+ ## フィールドのまとめ方 (group)
37
+
38
+ 各フィールドに \`group\` を付ける。これはフォーム上で横に並べる単位になる。
39
+
40
+ - **意味のまとまり**で付ける。フィールド型ではなく、業務上の役割で考える。
41
+ 例: 書籍管理なら「書誌情報」(出版社・出版年・ISBN)、「貸出」(貸出先・返却予定日)、
42
+ 「分類」(ジャンル・タグ)、「所在」(棚・段) のように。
43
+ - **同じ \`group\` のフィールドは必ず続けて並べる**。離れていると検証で弾かれる。
44
+ - 1 つの \`group\` は 2〜3 個が目安。多すぎると横一列に収まらない。
45
+ - まとめる相手がいないフィールドにも、それ単体の \`group\` を付けてよい。
46
+ - \`group\` の名前は日本語の短い語で。フォームには表示されないので、業務で通じる言葉にする。
47
+
48
+ ## フィールドコード (code)
49
+
50
+ - 新しく作るフィールドは \`code\` を空文字にする。\`label\` から自動で決まる。
51
+
52
+ ## kintone 固有の制約 (守らないと検証で弾かれます)
53
+
54
+ - 選択肢は必ず \`options: ["高", "中", "低"]\` のように文字列の配列で書く。索引付きのオブジェクトにはしない。
55
+ - defaultValue を指定する場合、その値は options に含まれていなければならない。
56
+ RADIO_BUTTON と DROP_DOWN の defaultValue は文字列、CHECK_BOX と MULTI_SELECT は配列。
57
+ - 次の型は指定できない: ${Object.keys(UNADDABLE_FIELD_TYPES).join(", ")}。
58
+ ステータス管理が必要な場合は DROP_DOWN か RADIO_BUTTON で代用する。
59
+ レコード番号・作成者・作成日時・更新者・更新日時はアプリ作成時に自動で用意されるので fields に含めない。
60
+ - code (フィールドコード) は省略してよい。省略した場合は label から自動で導出される。
61
+ 明示する場合、先頭に数字は使えず、空白や記号 (( ) % / \\ など) も使えない。日本語は使える。
62
+ - 一覧 (views) の fields に指定できるのは、自分で定義したフィールドコードと、
63
+ 組み込みの「レコード番号」「作成者」「作成日時」「更新者」「更新日時」だけ。
64
+ - アプリ名は 64 文字以内、説明は 10,000 文字以内。
65
+
66
+ ## 一覧 (views)
67
+
68
+ - 一覧は表形式 (LIST) のみ。カレンダー形式は指定できないので、そういう名前も付けない。
69
+ - 要件から自然に導ける切り口があるときだけ一覧を定義する。無理に作らない。
70
+ - 一覧を定義する場合、少なくとも 1 つは全件が見えるものを含める。
71
+ - fields には、その一覧で見たいフィールド名を並べる。
72
+
73
+ ## アイコン (icon)
74
+
75
+ - アプリの内容を一目で表す絵文字を 1 文字だけ指定する (例: 書籍管理なら 📚、案件管理なら 💼)。
76
+ - 肌の色や ZWJ で連結した複雑な絵文字は避け、単体の絵文字を選ぶ。
77
+ - 適切な絵文字が思い当たらない場合は省略してよい。
78
+
79
+ ## 一般設定 (settings)
80
+
81
+ - titleFieldCode には、レコード一覧でその 1 件を識別できるフィールド (案件名、氏名など) を指定する。`;
82
+ /** 利用可能なテーマ。プロンプトの一部としてもユーザー向けヘルプとしても使う。 */
83
+ export const AVAILABLE_THEMES = APP_THEMES;
84
+ /**
85
+ * 構造化出力が使えない場合に、JSON の形を言葉で指示するための追記。
86
+ * 構造化出力のスキーマ上限に引っかかったときの退避経路で使う。
87
+ */
88
+ export const JSON_SHAPE_INSTRUCTION = `
89
+
90
+ ## 出力形式
91
+
92
+ 次の形の JSON だけを出力してください。前置きも説明も、コードブロックの記号も付けないでください。
93
+ 省略したい項目は、文字列なら空文字 ""、配列なら空配列 [] にしてください。
94
+
95
+ {
96
+ "name": "アプリ名",
97
+ "description": "アプリの説明",
98
+ "theme": "WHITE | RED | GREEN | BLUE | YELLOW | BLACK",
99
+ "icon": "絵文字1文字",
100
+ "titleFieldCode": "レコードのタイトルに使うフィールド名",
101
+ "fields": [
102
+ {
103
+ "type": "フィールド型",
104
+ "label": "フィールド名",
105
+ "required": true,
106
+ "options": ["選択肢1", "選択肢2"],
107
+ "expression": "CALC の計算式",
108
+ "group": "意味のまとまりの名前"
109
+ }
110
+ ],
111
+ "views": [
112
+ { "name": "一覧名", "fields": ["フィールド名"] }
113
+ ]
114
+ }`;
115
+ /**
116
+ * 既存アプリを直すときに、システムプロンプトへ足す指示。
117
+ *
118
+ * 更新でいちばん壊れやすいのは「既存フィールドのコードが変わること」。
119
+ * コードが変わると別のフィールドとみなされ、追加 + 元は削除候補になり、
120
+ * データが新しいフィールドには入らない。ここを強く縛る。
121
+ */
122
+ export const REVISE_INSTRUCTION = `
123
+
124
+ ## 既存アプリを直すときの決まり
125
+
126
+ いま動いているアプリの設計を渡します。**変更後の完成形**を、全体として出力してください。
127
+ 差分ではなく、フィールドを 1 つも落とさずに、あるべき姿を丸ごと書きます。
128
+
129
+ ### 守ること
130
+
131
+ - **既存フィールドの \`code\` は絶対に変えない。** 渡された \`code\` をそのまま書き写す。
132
+ ラベルを変えたい場合も \`code\` はそのまま。変えるとデータが引き継がれません。
133
+ - 新しく足すフィールドだけ \`code\` を空文字にする。
134
+ - **フィールドの型は後から変更できません。** 型を変えたい要望があっても、既存フィールドの
135
+ \`type\` は変えないこと。どうしても必要なら、新しいフィールドを足したうえで、
136
+ 古いフィールドを出力から外す (外したフィールドは削除候補に移り、データは残ります)。
137
+ - 変更を頼まれていないフィールドは、渡されたとおりに書き写す。勝手に消したり増やしたりしない。
138
+ - **一覧 (views) も、変更を頼まれていなければ渡されたとおりに書き写す。**
139
+ 並び順や絞り込み条件を勝手に足したり変えたりしない。
140
+ - **フィールドを外したら、一覧 (views) の fields からも外す。** 残っていると検証で弾かれます。
141
+ - 依頼が曖昧なときは、いま在るものを保つ方向に倒す。`;
142
+ //# sourceMappingURL=prompt.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prompt.js","sourceRoot":"","sources":["../../src/llm/prompt.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,OAAO,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAEpF;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG;;;;;;;;;;;;;UAanB,qBAAqB,CAAC,IAAI,CAAC,IAAI,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gBAiC1B,MAAM,CAAC,IAAI,CAAC,qBAAqB,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;;;;;;;;;;;;;;;;;;;;;;;;+DAwBE,CAAC;AAEhE,4CAA4C;AAC5C,MAAM,CAAC,MAAM,gBAAgB,GAAG,UAAU,CAAC;AAE3C;;;GAGG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;EA0BpC,CAAC;AAEH;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG;;;;;;;;;;;;;;;;;;;4BAmBN,CAAC"}
@@ -0,0 +1,283 @@
1
+ import { z } from "zod";
2
+ import { DEFAULT_MAX_PER_ROW } from "./layout.js";
3
+ import { deriveFieldCode, fieldSpecSchema, UNADDABLE_FIELD_TYPES, validateFieldCode, } from "./fieldSpec.js";
4
+ export const APP_THEMES = [
5
+ "WHITE",
6
+ "RED",
7
+ "GREEN",
8
+ "BLUE",
9
+ "YELLOW",
10
+ "BLACK",
11
+ ];
12
+ /** 一覧 (ビュー) の定義。v1 は一覧形式とカレンダー形式のみ扱う。 */
13
+ export const viewSpecSchema = z
14
+ .object({
15
+ /** 一覧の名前。64 文字以内。 */
16
+ name: z.string().min(1).max(64),
17
+ type: z.enum(["LIST", "CALENDAR"]).default("LIST"),
18
+ /** LIST のとき表示するフィールドコード。省略時は全フィールドを表示する。 */
19
+ fields: z.array(z.string().min(1)).optional(),
20
+ /** CALENDAR のとき日付として使うフィールドコード。 */
21
+ date: z.string().min(1).optional(),
22
+ /** CALENDAR のときタイトルとして使うフィールドコード。 */
23
+ title: z.string().min(1).optional(),
24
+ /** 絞り込み条件 (kintone のクエリ形式)。例: `状態 in ("対応中")` */
25
+ filterCond: z.string().optional(),
26
+ /** 並び順 (kintone のクエリ形式)。例: `更新日時 desc` */
27
+ sort: z.string().optional(),
28
+ /** ページネーションを有効にするか。 */
29
+ pager: z.boolean().optional(),
30
+ device: z.enum(["DESKTOP", "ANY"]).optional(),
31
+ })
32
+ .strict();
33
+ /** アプリの一般設定。 */
34
+ export const generalSettingsSchema = z
35
+ .object({
36
+ /** レコードのタイトルに使うフィールドコード。省略時は kintone の自動選択。 */
37
+ titleFieldCode: z.string().min(1).optional(),
38
+ enableComments: z.boolean().optional(),
39
+ enableThumbnails: z.boolean().optional(),
40
+ enableBulkDeletion: z.boolean().optional(),
41
+ enableDuplicateRecord: z.boolean().optional(),
42
+ enableInlineRecordEditing: z.boolean().optional(),
43
+ /** 年度の開始月 (1-12)。 */
44
+ firstMonthOfFiscalYear: z.number().int().min(1).max(12).optional(),
45
+ })
46
+ .strict();
47
+ const appSpecObjectSchema = z
48
+ .object({
49
+ /** アプリ名。kintone の上限に合わせて 64 文字以内。 */
50
+ name: z.string().min(1).max(64),
51
+ /** アプリの説明。kintone の上限に合わせて 10,000 文字以内。 */
52
+ description: z.string().max(10000).optional(),
53
+ theme: z.enum(APP_THEMES).optional(),
54
+ /**
55
+ * アプリアイコンにする絵文字、または頭文字 (1〜3 文字)。
56
+ * 画像を生成してアップロードし、アプリアイコンとして設定する。
57
+ * 省略した場合はアイコンを設定しない (kintone の既定アイコンのまま)。
58
+ */
59
+ icon: z.string().min(1).refine((value) => [...value].length <= 8, {
60
+ message: "icon は絵文字 1 文字か、短い頭文字で指定してください",
61
+ }).optional(),
62
+ /**
63
+ * アプリを作成するスペース ID。省略するとスペースに属さないアプリになる。
64
+ * CLI の --space を付けるとこちらより優先される。
65
+ */
66
+ space: z.union([z.number().int().positive(), z.string().min(1)]).optional(),
67
+ /** スペース内のスレッド ID。kintone がスレッドの指定を求める場合に使う。 */
68
+ thread: z.union([z.number().int().positive(), z.string().min(1)]).optional(),
69
+ /** ゲストスペース ID。ゲストスペースのアプリは API のパスが変わる。 */
70
+ guestSpaceId: z.union([z.number().int().positive(), z.string().min(1)]).optional(),
71
+ /** アイコンの背景色 (#rrggbb)。省略時はアプリ名から決まる。 */
72
+ iconBackground: z
73
+ .string()
74
+ .regex(/^#[0-9a-fA-F]{6}$/, "iconBackground は #rrggbb 形式で指定してください")
75
+ .optional(),
76
+ /** フィールド。配列の順序がそのままフォームの配置順になる。 */
77
+ fields: z.array(fieldSpecSchema).min(1, "フィールドを 1 つ以上指定してください"),
78
+ /**
79
+ * フォームの並べ方。
80
+ * `grouped` (既定) は、似た系統のフィールドを横に並べる。
81
+ * `stacked` はレイアウトに手を加えず、1 行 1 フィールドのままにする。
82
+ */
83
+ layout: z
84
+ .union([
85
+ z.enum(["grouped", "stacked"]),
86
+ z
87
+ .object({
88
+ mode: z.enum(["grouped", "stacked"]).default("grouped"),
89
+ /** 1 行に並べる上限。 */
90
+ maxPerRow: z.number().int().min(1).max(4).default(DEFAULT_MAX_PER_ROW),
91
+ })
92
+ .strict(),
93
+ ])
94
+ .optional(),
95
+ /** 一覧。省略した場合は既定の一覧に手を加えない。 */
96
+ views: z.array(viewSpecSchema).optional(),
97
+ settings: generalSettingsSchema.optional(),
98
+ })
99
+ .strict();
100
+ /**
101
+ * AppSpec のスキーマ。
102
+ * フィールドコードの重複や、一覧・一般設定から参照されるフィールドコードの存在確認まで含めて、
103
+ * kintone に 1 リクエストも投げる前に検出する。
104
+ */
105
+ export const appSpecSchema = appSpecObjectSchema.superRefine((spec, ctx) => {
106
+ const codes = new Set();
107
+ spec.fields.forEach((field, index) => {
108
+ const code = resolveFieldCode(field);
109
+ const issue = validateFieldCode(code);
110
+ if (issue) {
111
+ ctx.addIssue({
112
+ code: z.ZodIssueCode.custom,
113
+ path: ["fields", index, field.code === undefined ? "label" : "code"],
114
+ message: field.code === undefined
115
+ ? `label から導出したフィールドコード "${code}" が不正です: ${issue.reason}`
116
+ : issue.reason,
117
+ });
118
+ return;
119
+ }
120
+ if (codes.has(code)) {
121
+ ctx.addIssue({
122
+ code: z.ZodIssueCode.custom,
123
+ path: ["fields", index, "code"],
124
+ message: `フィールドコード "${code}" が重複しています`,
125
+ });
126
+ }
127
+ codes.add(code);
128
+ });
129
+ spec.views?.forEach((view, index) => {
130
+ const referenced = [
131
+ ["date", view.date],
132
+ ["title", view.title],
133
+ ];
134
+ for (const [key, code] of referenced) {
135
+ if (code !== undefined && !codes.has(code)) {
136
+ ctx.addIssue({
137
+ code: z.ZodIssueCode.custom,
138
+ path: ["views", index, key],
139
+ message: `フィールドコード "${code}" は fields に存在しません`,
140
+ });
141
+ }
142
+ }
143
+ view.fields?.forEach((code, fieldIndex) => {
144
+ if (!codes.has(code) && !BUILT_IN_FIELD_CODES.has(code)) {
145
+ ctx.addIssue({
146
+ code: z.ZodIssueCode.custom,
147
+ path: ["views", index, "fields", fieldIndex],
148
+ message: `フィールドコード "${code}" は fields に存在しません`,
149
+ });
150
+ }
151
+ });
152
+ if (view.type === "CALENDAR" && view.date === undefined) {
153
+ ctx.addIssue({
154
+ code: z.ZodIssueCode.custom,
155
+ path: ["views", index, "date"],
156
+ message: "CALENDAR 形式の一覧には date が必要です",
157
+ });
158
+ }
159
+ });
160
+ const titleFieldCode = spec.settings?.titleFieldCode;
161
+ if (titleFieldCode !== undefined && !codes.has(titleFieldCode)) {
162
+ ctx.addIssue({
163
+ code: z.ZodIssueCode.custom,
164
+ path: ["settings", "titleFieldCode"],
165
+ message: `フィールドコード "${titleFieldCode}" は fields に存在しません`,
166
+ });
167
+ }
168
+ // 同じ group が離れて書かれていると、まとまらずに散らばった行になる。
169
+ // 気付きにくいので、デプロイ前に指摘する。
170
+ const seenGroups = new Map();
171
+ let previousGroup;
172
+ spec.fields.forEach((field, index) => {
173
+ const group = field.group;
174
+ if (group !== undefined && group !== previousGroup && seenGroups.has(group)) {
175
+ ctx.addIssue({
176
+ code: z.ZodIssueCode.custom,
177
+ path: ["fields", index, "group"],
178
+ message: `group "${group}" のフィールドが離れて書かれています ` +
179
+ `(${seenGroups.get(group) + 1} 番目のフィールドにも同じ group があります)。` +
180
+ " 同じ group のフィールドは続けて並べてください。",
181
+ });
182
+ }
183
+ if (group !== undefined)
184
+ seenGroups.set(group, index);
185
+ previousGroup = group;
186
+ });
187
+ const viewNames = new Set();
188
+ spec.views?.forEach((view, index) => {
189
+ if (viewNames.has(view.name)) {
190
+ ctx.addIssue({
191
+ code: z.ZodIssueCode.custom,
192
+ path: ["views", index, "name"],
193
+ message: `一覧名 "${view.name}" が重複しています`,
194
+ });
195
+ }
196
+ viewNames.add(view.name);
197
+ });
198
+ });
199
+ /** アプリ作成時に自動生成され、一覧に指定できる組み込みフィールドコード。 */
200
+ export const BUILT_IN_FIELD_CODES = new Set([
201
+ "レコード番号",
202
+ "作成者",
203
+ "作成日時",
204
+ "更新者",
205
+ "更新日時",
206
+ "Record_number",
207
+ "Created_by",
208
+ "Created_datetime",
209
+ "Updated_by",
210
+ "Updated_datetime",
211
+ ]);
212
+ /** フィールドの実効コードを返す (未指定なら label から導出)。 */
213
+ export function resolveFieldCode(field) {
214
+ return field.code ?? deriveFieldCode(field.label);
215
+ }
216
+ /** AppSpec に対する検証エラー。どのパスで何が起きたかを人が読める形で保持する。 */
217
+ export class AppSpecValidationError extends Error {
218
+ issues;
219
+ constructor(message, issues) {
220
+ super(message);
221
+ this.issues = issues;
222
+ this.name = "AppSpecValidationError";
223
+ }
224
+ }
225
+ /**
226
+ * 任意の入力を AppSpec として検証する。
227
+ * サポート外のフィールド型は Zod のメッセージだけだと理由が伝わらないため、
228
+ * 先に既知の「追加できない型」を検出して個別のメッセージを出す。
229
+ */
230
+ export function parseAppSpec(input) {
231
+ const preIssues = detectUnsupportedFieldTypes(input);
232
+ if (preIssues.length > 0) {
233
+ throw new AppSpecValidationError("AppSpec の検証に失敗しました", preIssues);
234
+ }
235
+ const result = appSpecSchema.safeParse(input);
236
+ if (!result.success) {
237
+ throw new AppSpecValidationError("AppSpec の検証に失敗しました", result.error.issues.map((issue) => ({
238
+ path: issue.path.length > 0 ? issue.path.join(".") : "(root)",
239
+ message: issue.message,
240
+ })));
241
+ }
242
+ return result.data;
243
+ }
244
+ function detectUnsupportedFieldTypes(input) {
245
+ if (typeof input !== "object" || input === null)
246
+ return [];
247
+ const fields = input.fields;
248
+ if (!Array.isArray(fields))
249
+ return [];
250
+ const issues = [];
251
+ fields.forEach((field, index) => {
252
+ if (typeof field !== "object" || field === null)
253
+ return;
254
+ const type = field.type;
255
+ if (typeof type !== "string")
256
+ return;
257
+ const reason = UNADDABLE_FIELD_TYPES[type];
258
+ if (reason !== undefined) {
259
+ issues.push({ path: `fields.${index}.type`, message: `${type}: ${reason}` });
260
+ }
261
+ });
262
+ return issues;
263
+ }
264
+ /** フィールドコード → group の対応。レイアウトの組み立てに渡す。 */
265
+ export function fieldGroups(spec) {
266
+ const groups = {};
267
+ for (const field of spec.fields) {
268
+ if (field.group !== undefined) {
269
+ groups[resolveFieldCode(field)] = field.group;
270
+ }
271
+ }
272
+ return groups;
273
+ }
274
+ /** layout の指定を、扱いやすい形に正規化する。 */
275
+ export function resolveLayout(spec) {
276
+ const layout = spec.layout;
277
+ if (layout === undefined)
278
+ return { mode: "grouped", maxPerRow: DEFAULT_MAX_PER_ROW };
279
+ if (typeof layout === "string")
280
+ return { mode: layout, maxPerRow: DEFAULT_MAX_PER_ROW };
281
+ return { mode: layout.mode, maxPerRow: layout.maxPerRow };
282
+ }
283
+ //# sourceMappingURL=appSpec.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"appSpec.js","sourceRoot":"","sources":["../../src/spec/appSpec.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,218 @@
1
+ import { fieldGroups, resolveFieldCode, resolveLayout } from "./appSpec.js";
2
+ /** 変更が 1 つも無いか。 */
3
+ export function isEmptyDiff(diff) {
4
+ return (diff.added.length === 0 &&
5
+ diff.updated.length === 0 &&
6
+ diff.retyped.length === 0 &&
7
+ diff.orphaned.length === 0 &&
8
+ diff.app.length === 0 &&
9
+ diff.views.added.length === 0 &&
10
+ diff.views.updated.length === 0 &&
11
+ diff.views.removed.length === 0 &&
12
+ // 適用されない並びの違いは、差分として数えない (何も起きないため)。
13
+ !diff.layout.willApply);
14
+ }
15
+ /** 目標に近づけるために kintone を変更する必要があるか (削除候補への退避も変更に含む)。 */
16
+ export function hasWork(diff) {
17
+ return !isEmptyDiff(diff);
18
+ }
19
+ export function diffAppSpec(current, desired) {
20
+ const currentFields = byCode(current.fields);
21
+ const desiredFields = byCode(desired.fields);
22
+ const added = [];
23
+ const updated = [];
24
+ const retyped = [];
25
+ const orphaned = [];
26
+ for (const [code, field] of desiredFields) {
27
+ const existing = currentFields.get(code);
28
+ if (existing === undefined) {
29
+ added.push({ code, field });
30
+ continue;
31
+ }
32
+ if (existing.type !== field.type) {
33
+ // kintone は作成後の型を変えられない。呼び出し側で扱いを決めてもらう。
34
+ retyped.push({ code, from: existing.type, to: field.type });
35
+ continue;
36
+ }
37
+ const changes = compareFields(existing, field);
38
+ if (changes.length > 0) {
39
+ updated.push({ code, field, changes });
40
+ }
41
+ }
42
+ for (const [code, field] of currentFields) {
43
+ if (!desiredFields.has(code)) {
44
+ orphaned.push({ code, type: field.type, label: field.label });
45
+ }
46
+ }
47
+ return {
48
+ added,
49
+ updated,
50
+ retyped,
51
+ orphaned,
52
+ app: compareAppSettings(current, desired),
53
+ views: compareViews(current.views ?? [], desired.views ?? []),
54
+ layout: compareLayout(current, desired),
55
+ };
56
+ }
57
+ function compareLayout(current, desired) {
58
+ const currentLayout = resolveLayout(current);
59
+ const desiredLayout = resolveLayout(desired);
60
+ const describe = (layout) => layout.mode === "grouped" ? `grouped (最大 ${layout.maxPerRow} 列)` : "stacked";
61
+ const currentCodes = current.fields.map(resolveFieldCode);
62
+ const desiredCodes = desired.fields.map(resolveFieldCode);
63
+ const shared = new Set(currentCodes.filter((code) => desiredCodes.includes(code)));
64
+ const orderChanged = JSON.stringify(currentCodes.filter((code) => shared.has(code))) !==
65
+ JSON.stringify(desiredCodes.filter((code) => shared.has(code)));
66
+ const currentGroups = fieldGroups(current);
67
+ const desiredGroups = fieldGroups(desired);
68
+ const groupsChanged = [...shared].some((code) => currentGroups[code] !== desiredGroups[code]);
69
+ const modeChanged = currentLayout.mode !== desiredLayout.mode || currentLayout.maxPerRow !== desiredLayout.maxPerRow;
70
+ return {
71
+ from: describe(currentLayout),
72
+ to: describe(desiredLayout),
73
+ modeChanged,
74
+ orderChanged,
75
+ groupsChanged,
76
+ // stacked は「既存の並びに手を触れない」指定なので、違いがあっても組み直さない。
77
+ willApply: desiredLayout.mode === "grouped" && (modeChanged || orderChanged || groupsChanged),
78
+ };
79
+ }
80
+ function byCode(fields) {
81
+ return new Map(fields.map((field) => [resolveFieldCode(field), field]));
82
+ }
83
+ /**
84
+ * フィールドの設定を比べる。
85
+ * `code` と `group` は比較から外す — 前者は同一性の鍵、
86
+ * 後者はレイアウトの都合で kintone には送らない情報なので。
87
+ *
88
+ * **AppSpec で書かれていない項目は「現状維持」として扱う。**
89
+ * kintone の設定変更 API は省略した項目を変えないので、
90
+ * 「現状にはあるが目標には書かれていない」を変更として報告すると、
91
+ * 実際には何も起きないのに差分が永遠に消えないことになる。
92
+ * 値を変えたいときは、明示的に書く (例: `"required": false`)。
93
+ */
94
+ const FIELD_KEYS_TO_IGNORE = new Set(["code", "type", "group"]);
95
+ function compareFields(current, desired) {
96
+ const keys = new Set([
97
+ ...Object.keys(current).filter((key) => !FIELD_KEYS_TO_IGNORE.has(key)),
98
+ ...Object.keys(desired).filter((key) => !FIELD_KEYS_TO_IGNORE.has(key)),
99
+ ]);
100
+ const changes = [];
101
+ for (const key of [...keys].sort()) {
102
+ const from = current[key];
103
+ const to = desired[key];
104
+ if (to === undefined)
105
+ continue;
106
+ if (!same(from, to)) {
107
+ changes.push({ key, from, to });
108
+ }
109
+ }
110
+ return changes;
111
+ }
112
+ function compareAppSettings(current, desired) {
113
+ const changes = [];
114
+ // 未指定は「現状維持」。書かれていない項目は送らないので、差分にもしない。
115
+ for (const key of ["name", "description", "theme", "icon", "iconBackground"]) {
116
+ if (desired[key] === undefined)
117
+ continue;
118
+ if (!same(current[key], desired[key])) {
119
+ changes.push({ key, from: current[key], to: desired[key] });
120
+ }
121
+ }
122
+ const currentSettings = (current.settings ?? {});
123
+ const desiredSettings = (desired.settings ?? {});
124
+ for (const key of [...new Set([...Object.keys(currentSettings), ...Object.keys(desiredSettings)])].sort()) {
125
+ if (desiredSettings[key] === undefined)
126
+ continue;
127
+ if (!same(currentSettings[key], desiredSettings[key])) {
128
+ changes.push({ key: `settings.${key}`, from: currentSettings[key], to: desiredSettings[key] });
129
+ }
130
+ }
131
+ return changes;
132
+ }
133
+ /** 一覧は名前で対応付ける。 */
134
+ function compareViews(current, desired) {
135
+ const currentByName = new Map(current.map((view) => [view.name, view]));
136
+ const desiredByName = new Map(desired.map((view) => [view.name, view]));
137
+ const added = [];
138
+ const updated = [];
139
+ for (const [name, view] of desiredByName) {
140
+ const existing = currentByName.get(name);
141
+ if (existing === undefined) {
142
+ added.push(view);
143
+ continue;
144
+ }
145
+ const changes = [];
146
+ const keys = new Set([...Object.keys(existing), ...Object.keys(view)]);
147
+ for (const key of [...keys].sort()) {
148
+ if (key === "name")
149
+ continue;
150
+ const from = existing[key];
151
+ const to = view[key];
152
+ if (!same(from, to))
153
+ changes.push({ key, from, to });
154
+ }
155
+ if (changes.length > 0)
156
+ updated.push({ name, view, changes });
157
+ }
158
+ const removed = [...currentByName.keys()].filter((name) => !desiredByName.has(name));
159
+ return { added, updated, removed };
160
+ }
161
+ /**
162
+ * 値を比べる。
163
+ *
164
+ * 真偽値の設定は kintone の既定が false なので、**未指定と false は同じ**とみなす。
165
+ * ここを分けると「required を書いていないフィールド」と
166
+ * 「required: false と書いたフィールド」が毎回差分になり、無意味な変更が出続ける。
167
+ */
168
+ function same(a, b) {
169
+ if (a === b)
170
+ return true;
171
+ if (a === undefined)
172
+ return b === false;
173
+ if (b === undefined)
174
+ return a === false;
175
+ return JSON.stringify(a) === JSON.stringify(b);
176
+ }
177
+ /** 差分を人が読める行にする。 */
178
+ export function describeDiff(diff) {
179
+ const lines = [];
180
+ const show = (value) => value === undefined ? "(なし)" : typeof value === "string" ? value : JSON.stringify(value);
181
+ for (const change of diff.app) {
182
+ lines.push(` ~ ${change.key}: ${show(change.from)} → ${show(change.to)}`);
183
+ }
184
+ for (const addition of diff.added) {
185
+ lines.push(` + ${addition.code} (${addition.field.type}) を追加`);
186
+ }
187
+ for (const update of diff.updated) {
188
+ const detail = update.changes
189
+ .map((change) => `${change.key}: ${show(change.from)} → ${show(change.to)}`)
190
+ .join(", ");
191
+ lines.push(` ~ ${update.code}: ${detail}`);
192
+ }
193
+ for (const retype of diff.retyped) {
194
+ lines.push(` ! ${retype.code}: 型を ${retype.from} → ${retype.to} に変更 (kintone では不可)`);
195
+ }
196
+ for (const orphan of diff.orphaned) {
197
+ lines.push(` - ${orphan.code} (${orphan.type}) を削除候補へ`);
198
+ }
199
+ if (diff.layout.willApply) {
200
+ const reasons = [
201
+ diff.layout.modeChanged ? `指定: ${diff.layout.from} → ${diff.layout.to}` : null,
202
+ diff.layout.orderChanged ? "並び順" : null,
203
+ diff.layout.groupsChanged ? "group" : null,
204
+ ].filter((reason) => reason !== null);
205
+ lines.push(` ~ フォームの並びを組み直す (${reasons.join(", ")})`);
206
+ }
207
+ for (const view of diff.views.added) {
208
+ lines.push(` + 一覧「${view.name}」を追加`);
209
+ }
210
+ for (const view of diff.views.updated) {
211
+ lines.push(` ~ 一覧「${view.name}」: ${view.changes.map((c) => c.key).join(", ")}`);
212
+ }
213
+ for (const name of diff.views.removed) {
214
+ lines.push(` - 一覧「${name}」を削除`);
215
+ }
216
+ return lines;
217
+ }
218
+ //# sourceMappingURL=diff.js.map