ほとんどのドキュメンテーション・プラットフォームは、航空会社が "レッグルーム "について話すように、"拡張性 "について話します。技術的には存在しますが、実質的には期待はずれです。私はラセピのアーキテクチャが予測不可能になることなく、純粋に拡張可能であることを望みました:プラグインで能力を、アクションガードで制御を、そしてパイプラインで決定論的な実行を。
この投稿では、それぞれが実際のコードベースでどのように動作するかを説明します。
プラグインシステム: モジュール設計
Rasepiの全てのプラグインは、IPluginModuleを実装します。これは、プラグインが何であるか、どのようなサービスが必要か、どのようなルートを公開するかを宣言する単一のインターフェースです:
CODEBLOCK_0__
CODEBLOCK_24__は純粋なデータです。CODEBLOCK_24__は純粋なデータで、何も実行せずにプラグインを記述します:
コードブロック_1__
CODEBLOCK_25__に注目してください。この辞書は、フロントエンドの拡張ポイントをコンポーネント名にマップします。これにより、Vueフロントエンドは、各プラグインがどのUIコンポーネント(ツールバーのボタン、サイドバーのパネル、設定ページ)に寄与するかを知ることができます。
登録はプラグインごとに1行です。
起動時に、流暢なAPIを通じてプラグインを登録します:
codeblock_2__
それぞれの呼び出しはモジュールをインスタンス化し、レジストリに保存し、RegisterServices()を呼び出して依存関係を設定します。アプリがビルドされると、1行ですべてのプラグインのルートがマップされます:
CODEBLOCK_3__
プラグインは/api/plugins/{pluginId}/でスコープされたルートグループを取得し、認可が自動的に適用されます。
実際の例: Workflowプラグイン
実際のプラグインであるWorkflow & Approvalsモジュールの例を示します:
コードブロック_4__
コア・プラットフォームはWorkflowServiceやWorkflowPublishGuardを直接参照することはありません。DIコンテナを通してそれらを検出します。これがゼロカップリングの鍵です。コアアプリはプラグインのコードに触れることはありません。
アクションガード:コントロールレイヤー
プラグインはケイパビリティを追加します。アクションガードは、そのケイパビリティやコアのアクションが実行されるかどうかを決定します。アクションガードは同期バリデータであり、実行前に処理をインターセプトします。
アクションガードの評価フロー](/ja/blog/img/action-guard-flow.svg)
インターフェイスは意図的に最小化されています:
コードブロック_5__
CODEBLOCK_30__がnullの場合、ガードは全てのアクションに対して実行されます。それが"Entry.Publish"のように設定されている場合、その特定のアクションだけをインターセプトします。
コンテキストと結果の契約
すべてのガードは、アクション名、テナント、ユーザー、エンティティ、プロパティバッグを含む型付きコンテキストを受け取ります:
codeblock_6__
そして、すべてのガードは予測可能な結果を返します: allow、deny、またはallow-with-modificationsです:
codeblock_7。
CODEBLOCK_33__フィールドは重要です。ガードはアクションを承認しますが、コンテンツの一部を書き換えることができます(例えば、公開前に秘密を再編集するなど)。
正規のアクション名
ガードが何をターゲットにできるかについて曖昧さがないように、すべてのインターセプト可能なアクションを文字列定数として定義します:
codeblock_8__
実際の例: 承認のない公開のブロック
Workflow プラグインは Entry.Publish を阻止するガードを登録します:
コードブロック_9__
コアプラットフォームは承認ワークフローについて何も知りません。パイプラインを通してEntry.Publishを呼び出すだけで、ワークフローが完了していなければガードはそれをブロックします。
アクションパイプライン: すべてが収束する場所
CODEBLOCK_36__は全てのガードされた操作のための単一の実行パスです。CODEBLOCK_36__はどのガードが適用されるかを決定し、それらを評価し、アクションをブロックするか実行します。
CODEBLOCK_10__
CODEBLOCK_37__メソッドは重い仕事をします:
コードブロック_11__
ここで3つの重要な設計上の決定があります:
1.1. テナントごとの解決. TenantPluginResolverは、各テナントがどのプラグインをインストールして有効にしているかをチェックします。無効なプラグインのガードは決して実行されません。
2.All-must-pass. ガードが拒否された場合、アクションはブロックされます。これは意図的なセキュリティスタンスです。
3.**ガードが例外をスローした場合、それはログに記録され、Allow()として扱われます。これは壊れたプラグインがプラットフォーム全体をロックするのを防ぎます。
テナントごとのプラグイン解決
リゾルバはTenantPluginInstallationsテーブルをクエリします(EFグローバルクエリフィルタによって現在のテナントに自動的にスコープされます):
CODEBLOCK_12__
イベント駆動型の副作用
アクションは同期です。副作用は同期ではありません。アクションが完了すると、サービスはドメインイベントを発行します:
コードブロック_13__
イベントはメモリ内のチャネルにエンキューされ、バックグラウンドの EventConsumerWorker によって処理されます。ワーカーは複数のシステムにイベントをルーティングします:
- アクティビティトラッキング。
- Translation billing. プロバイダごとのコストを追跡します。
- プラグインのイベントハンドラ。
プラグインイベントハンドラはIPluginEventHandlerを実装しています:
コードブロック
Worker は、そのテナントでプラグインが有効になっているハンドラのみを呼び出します。つまり、プラグイン A の副作用が、プラグイン B しかインストールされていないテナントに漏れることはありません。
ブロックレベルの翻訳エンジン
このアーキテクチャーが最も目に見えて効果を発揮するのはここです。
ブロックレベル翻訳:変更されたブロックだけが再翻訳されます](/ja/blog/img/block-translation.svg)
従来のプラットフォームでは、文書全体を翻訳していました。私たちは、段落、見出し、リスト項目など、個々のブロックを翻訳します。ユーザーが50ブロックの文書の1つの段落を編集すると、その段落だけが再翻訳を必要とします。これが94%のコスト削減の源泉です。
TipTap JSONからのブロックの作成方法
ユーザーがドキュメントを保存すると、TipTapエディタは次のようなJSONを送信します:
codeblock_15__
CODEBLOCK_43__はこのJSONを解析し、個々のEntryBlockレコードを作成します:
コードブロック_16__
古いレコードを検出するためのSHA256ハッシュ
コンテンツハッシュは古さ検出の中核です。ブロックの内容(blockIdやdeletedのようなメタデータ属性を取り除いた後)をSHA256を使ってハッシュします:
コードブロック_17__
ソースブロックが変更されると、そのハッシュも変更されます。そして、システムはすべての翻訳ブロックのSourceContentHashと現在のソース・ハッシュを比較し、不一致はStaleとマークされます:
コードブロック
構造適応
翻訳者は言語間でブロックタイプを変更することができます。英語の箇条書きリストはドイツ語の番号付きリストになるかもしれません。システムはこれを追跡します:
コードブロック_19__
プラグインとしての翻訳プロバイダ
外部の翻訳サービス(DeepL、Google Translateなど)は、ITranslationProviderPluginを通してプラグインします:
コードブロック
バッチメソッドは、コンテンツへのブロックIDの辞書を受信し、それらをすべて翻訳し、課金文字数とともに翻訳を返します。ドキュメント全体ではなく、古くなったブロックだけを送信するので、コストは最小限に抑えられます。
テナントの分離: 見えないセーフティネット
上記の全てのシステムは厳格なテナント分離の中で稼働しています。
CODEBLOCK_50__はリクエスト毎にJWTからテナントを解決し、メンバーシップを検証します:
CODEBLOCK_21__
Entity Frameworkのグローバルクエリフィルタは、開発者がテナントによるフィルタリングを忘れても、データベースレイヤーが自動的に行うことを保証します:
コードブロック_22__
結果はCODEBLOCK_51__は常に現在のテナントのハブだけを返します。データ・リークには、クエリ・フィルターを積極的に回避する必要がありますが、私たちのコードベースではこれを禁止しています。
全体像
ユーザーがエントリーの "Publish "をクリックすると、次のようなことが起こります:
1.**認証がJWTを検証し、TenantContextMiddlewareが解決してテナントを検証します。
2.**コントローラがパイプラインを呼び出します。
3.**テナントが有効にしているプラグインを照会し、該当するガードを選択します。
4.**ワークフローガードは承認をチェックし、保持ガードはポリシーをチェックし、ルールガードはコンテンツを検証します。すべてパスしますか?エントリが公開されます。
5.**CODEBLOCK_54__ イベントがエンキューされます。バックグラウンドワーカーはアクティビティを記録し、翻訳課金を更新し、プラグインイベントハンドラを呼び出します。
6.**ブロックの翻訳がチェックされます。
各レイヤーは自分の仕事をします。どのレイヤーも他のレイヤーには届きません。それがアーキテクチャです。
拡張性が流行っているから作ったのではありません。各チームのワークフローに適応できないドキュメントプラットフォームは、いずれ適応できるものに取って代わられるからです。そして、ガードレールなしで適応するプラットフォームは、いずれ重要な何かを壊してしまうでしょう。