型と契約の共有Contracts
松葉は複数のアプリ・複数の言語に分かれています。アプリ同士がやり取りするとき、「やり取りするデータの形(型)」が双方でずれていると不具合の温床になります。たとえば送り手と受け手で項目名が 1 文字違うだけで、データが正しく渡りません。これを「気をつける」ではなく「構造的に起こらない」状態にするため、1 つの定義から各言語の型を自動生成し、画面とサーバの間も共通の検証ルールで繋ぎました。
1 つの定義から、2 言語の型を生むGenerate, don't duplicate
アプリ間でやり取りする「出来事(イベント)」の形は、protobuf(プロトバフ。データ構造を言語に依存しない形で記述する仕組み)で 1 か所に定義します。そこから Buf という道具で、TypeScript と Go の型を機械的に生成します。人が両方を手書きしないので、コピー忘れや書き間違いによるズレが起きません。
ワイヤ(実際に流れるメッセージ)は JSON です。 protobuf は「メッセージをバイナリで流すため」ではなく、型の自動生成とスキーマ(形)の統制のために使っています。流れるデータ自体は読みやすい JSON 形式なので、運用時に中身を目視で確認できます。
file.uploaded)」というイベントの形を 1 か所で定義しておくと、それを発行する側(業務 API・TypeScript)と受け取る側(検査・同期・Go)が同じ型を使います。後から項目を 1 つ増やしても、両言語の型が同時に更新されるため、片方だけ古いという状態が生まれません。
定義した 13 のイベントThirteen events
現時点で 13 種類のイベントを定義しました。4 つの定義ファイル(ファイル系・フォルダ系・共有系・Box 系)に分けています。名前({資源}.{動作} の形)と、それが業務上どんな出来事を表すかは次のとおりです。
| イベント名 | 業務上の出来事 |
|---|---|
ファイル系 — file.proto(6 種) | |
file.uploaded | ファイルのアップロードが完了した(記録・追跡用) |
file.scan_clean | ウイルス検査で問題なしと判定。ここから Drive 同期が始まる |
file.scan_infected | 感染を検出。隔離し、管理者へ通知 |
file.replaced | ファイルが新しい版に差し替えられた |
file.deleted_soft | ファイルが(ゴミ箱へ)論理削除された |
file.deleted_hard | 保持期間を過ぎ、物理的に削除された |
フォルダ系 — folder.proto(4 種) | |
folder.created | フォルダが作成された |
folder.renamed | フォルダ名が変更された |
folder.deleted_soft | フォルダが論理削除された |
folder.deleted_hard | フォルダが物理削除された |
共有系 — share.proto(2 種) | |
share.created | 共有リンクが発行された(受取人へ案内メール) |
share.accessed | 共有リンクがアクセスされた(監査記録用) |
Box 系 — box.proto(1 種) | |
box.drive_sync_toggled | Box の Drive 同期設定が ON / OFF された |
イベント名はシステムの「共通言語」です: これら 13 個の名前は、開発・運用・障害調査のすべてで使う共通の語彙になります。命名を {資源}.{動作} の形に統一し、1 か所のリストで管理することで、「人によって呼び方が違う」混乱を防いでいます。各イベントが実際にどう流れて処理されるかは「イベント駆動の整合性」「ユースケース別ワークフロー」で扱います。
画面とサーバも、同じルールで繋ぐFront meets back
アプリ間だけでなく、画面(ブラウザ側)とサーバ(API 側)の間でも、同じ型ズレの問題が起きます。松葉では @repo/shared という共通部品に、両者が使う検証ルール(Zod スキーマ。入力値が正しい形かを判定するルール)・定数・型を集約し、唯一の置き場としました。
@repo/shared に 1 つ定義しておくと、画面側の入力チェックもサーバ側の受け取りチェックも同じ値を参照します。「画面では 200 MB、サーバでは 100 MB」のような食い違いが起きません。
| 置き場(サブパス) | 役割(業務上の意味) |
|---|---|
entities | ユーザ・Box・ファイル・フォルダなどの「形」の定義(Zod スキーマ) |
schemas | API のリクエスト・レスポンスの検証ルール |
constants | 業務で決まっている値(例: ファイル上限 200 MB、既定の保持期間 180 日、監査記録の種別 21 種 など) |
types | 共通の型(API 応答・ページング・並び順など) |
utils | 共通の小さな処理(拡張子判定・日付整形など) |
errors | エラーの種類を表すコードの唯一の定義(画面とサーバで共通) |
設計上のこだわり: 決まった値しか取らない項目(状態・種別・権限など)は、文字列で手書きせず、データベース側の定義から型を引き継ぐ仕組みにしています。これにより「データベースには無い値を、うっかりコードに書いてしまう」ミスを、書いた時点で検出できます。画面とサーバの通信は、特別な通信ライブラリを使わず、標準的な通信(fetch)に薄いラッパーをかぶせた素直な作りにして、仕組みを単純に保っています。