0 フォロワー · 14 投稿

Angular(またはAngularJS)は、JavaScriptをベースにしたオープンソースのフロントエンドWebアプリケーションフレームワークであり、主にGoogleや個人や企業のコミュニティによって維持されており、単一ページアプリケーションの開発で直面する多くの課題に対応しています。 公式サイト

記事 Toshihiko Minamoto · 10月 9, 2025 8m read

少し遅れましたが、モバイルアプリケーションから接続する例を示して Workflow Engine に関する連載記事をようやく締めくくることにします。

前回の記事では、これから説明する例として、患者と担当医師の両方にとって高血圧症などの慢性病状の詳細な管理を可能にするアプリケーションを示しました。 この例では、患者は携帯電話からウェブアプリケーション(基本的に、デバイスに応答するように設計されたウェブページ)にアクセスし、ポータブル血圧計が IRIS インスタンスに送信する測定に基づく通知を受信します。

したがって、IRIS インスタンスへのアクセスは 2 つです。

  • モバイルアプリケーションからのユーザーアクセス。
  • 血圧の測定値を送信するデバイスアクセス。

この記事では、患者が測定値を生成するタスクを管理できる、最初のアクセスを確認します。

モバイルアプリケーションと IRIS の接続

この接続では、IRIS 内でウェブアプリケーションを構成するのが最も単純です。これを行うには、管理ポータルのシステム管理 -> セキュリティ -> アプリケーション -> ウェブアプリケーションからアクセスします。

次に、表示されるリストでアプリケーションの新規作成をクリックすると、以下のような画面が開きます。

この画面で、以下のフィールドを構成しましょう。

  • 名前: IRIS にデプロイされる機能にアクセスできるように公開する URL を定義します。
  • ネームスペース: ウェブアプリケーションを関連付けるネームスペースです。これによって、後で相互運用性プロダクションの機能を利用できるようになります。
  • REST: 公開するものが HTTP 接続を可能にする REST API であるため、このオプションを選択します。
  • ディスパッチクラス: HTTP 呼び出しを受け取り、その処理を決定する ObjectScript クラス。
  • JWT 認証を使用する: このオプションをオンにすると、アプリケーションに定義した URL の /login および /logout エンドポイントが有効になるため、IRIS への呼び出しを認証するための JSON ウェブトークンを取得できるようになります。
  • セキュリティ設定 -> 許可される認証方法: 呼び出しを安全にするためにパスワードを設定します。

Workflow.WS.Service クラスを確認しましょう。

Class Workflow.WS.Service Extends%CSP.REST
{

Parameter HandleCorsRequest = 0;Parameter CHARSET = "utf-8"; XData UrlMap [ XMLNamespace = "https://www.intersystems.com/urlmap" ] { <Routes> <Route Url="/getTasks" Method="GET" Call="GetTasks" /> <Route Url="/saveTask" Method="POST" Call="SaveTask" /> </Routes> }

ClassMethod OnHandleCorsRequest(url As%String) As%Status { set url = %request.GetCgiEnv("HTTP_REFERER") set origin = $p(url,"/",1,3) // origin = "http(s)://origin.com:port"// here you can check specific origins// otherway, it will allow all origins (useful while developing only)do%response.SetHeader("Access-Control-Allow-Credentials","true") do%response.SetHeader("Access-Control-Allow-Methods","GET,POST,PUT,DELETE,OPTIONS") do%response.SetHeader("Access-Control-Allow-Origin",origin) do%response.SetHeader("Access-Control-Allow-Headers","Access-Control-Allow-Origin, Origin, X-Requested-With, Content-Type, Accept, Authorization, Cache-Control") quit$$$OK }

ClassMethod GetTasks() As%Status { Try { Do##class(%REST.Impl).%SetContentType("application/json") If '##class(%REST.Impl).%CheckAccepts("application/json") Do##class(%REST.Impl).%ReportRESTError(..#HTTP406NOTACCEPTABLE,$$$ERROR($$$RESTBadAccepts)) QuitDo##class(%REST.Impl).%SetStatusCode("200") set sql = "SELECT %Actions, %Message, %Priority, %Subject, TaskStatus_TimeCreated, ID FROM EnsLib_Workflow.TaskResponse WHERE TaskStatus_AssignedTo = ? AND TaskStatus_IsComplete = 0"set statement = ##class(%SQL.Statement).%New(), statement.%ObjectSelectMode = 1set status = statement.%Prepare(sql) if ($$$ISOK(status)) { set resultSet = statement.%Execute($USERNAME) if (resultSet.%SQLCODE = 0) { set tasks = [] while (resultSet.%Next() '= 0) { set task = {"actions": "", "message": "", "priority": "", "subject": "", "creation": "", "id": ""} set task.actions = resultSet.%GetData(1) set task.message = resultSet.%GetData(2) set task.priority = resultSet.%GetData(3) set task.subject = resultSet.%GetData(4) set task.creation = resultSet.%GetData(5) set task.id = resultSet.%GetData(6) do tasks.%Push(task) }
} } set result = {"username": ""} set result.username = $USERNAMEDo##class(%REST.Impl).%WriteResponse(tasks)

} <span class="hljs-keyword">Catch</span> (ex) {
    <span class="hljs-keyword">Do</span> <span class="hljs-keyword">##class</span>(<span class="hljs-built_in">%REST.Impl</span>).<span class="hljs-built_in">%SetStatusCode</span>(<span class="hljs-string">"400"</span>)
    <span class="hljs-keyword">return</span> ex.DisplayString()
}

<span class="hljs-keyword">Quit</span> <span class="hljs-built_in">$$$OK</span>

}

ClassMethod SaveTask() As%Status { Try { Do##class(%REST.Impl).%SetContentType("application/json") If '##class(%REST.Impl).%CheckAccepts("application/json") Do##class(%REST.Impl).%ReportRESTError(..#HTTP406NOTACCEPTABLE,$$$ERROR($$$RESTBadAccepts)) Quit// Reading the body of the http call with the person dataset dynamicBody = {}.%FromJSON(%request.Content)

    <span class="hljs-keyword">set</span> task = <span class="hljs-keyword">##class</span>(EnsLib.Workflow.TaskResponse).<span class="hljs-built_in">%OpenId</span>(dynamicBody.<span class="hljs-built_in">%Get</span>(<span class="hljs-string">"id"</span>))
    <span class="hljs-keyword">set</span> sc = task.CompleteTask(dynamicBody.action)

    <span class="hljs-keyword">if</span> <span class="hljs-built_in">$$$ISOK</span>(sc) {	        
        <span class="hljs-keyword">Do</span> <span class="hljs-keyword">##class</span>(<span class="hljs-built_in">%REST.Impl</span>).<span class="hljs-built_in">%SetStatusCode</span>(<span class="hljs-string">"200"</span>)
        <span class="hljs-keyword">Do</span> <span class="hljs-keyword">##class</span>(<span class="hljs-built_in">%REST.Impl</span>).<span class="hljs-built_in">%WriteResponse</span>({<span class="hljs-string">"result"</span>: <span class="hljs-string">"success"</span>})         
	}	
    
} <span class="hljs-keyword">Catch</span> (ex) {
    <span class="hljs-keyword">Do</span> <span class="hljs-keyword">##class</span>(<span class="hljs-built_in">%REST.Impl</span>).<span class="hljs-built_in">%SetStatusCode</span>(<span class="hljs-string">"400"</span>)
    <span class="hljs-keyword">Do</span> <span class="hljs-keyword">##class</span>(<span class="hljs-built_in">%REST.Impl</span>).<span class="hljs-built_in">%WriteResponse</span>({<span class="hljs-string">"result"</span>: <span class="hljs-string">"error"</span>})
}

<span class="hljs-keyword">Quit</span> <span class="hljs-built_in">$$$OK</span>

}

}

ご覧のとおり、WS から必要なすべてのロジックを解決していますが、この方法は推奨されません。ネームスペースに構成されたプロダクションに受信したリクエストを送信することで可能なトレーサビリティを失ってしまうためです。

URLMap セクションを確認すると、2 つのエンドポイントが WS で構成されていることが分かります。

  • getTasks: ユーザーのすべての保留中のタスクを復旧します(使用されている SQL を見ると、ログインしたときに生成される変数から直接ユーザー名を渡しています)。
  • saveTask: ユーザーのタスクへのレスポンスを受信し、EnsLib.Workflow.TaskResponse クラスの CompleteTaks メソッドを実行することで、タスクの終了に進みます。

IRIS の部分では、外部アプリケーションが接続できるようにすべてが構成済みです。

Workflow Engine で外部アプリケーションをテストする

まず、ファイル /shared/hl7/message_1_1.hl7 をパス /shared/in にコピーすることで、HL7 からプロダクションへのメッセージの送信をシミュレーションします。送信するメッセージを確認しましょう。

MSH|^~\&|HIS|HULP|EMPI||20240402111716||ADT^A08|346831|P|2.5.1
EVN|A08|20240402111716
PID|||07751332X^^^MI^NI~900263^^^HULP^PI||LÓPEZ CABEZUELA^ÁLVARO^^^||19560121|F|||PASEO MARIO FERNÁNDEZ 2581 DERECHA^^MADRID^MADRID^28627^SPAIN||555819817^PRN^^ALVARO.LOPEZ@VODAFONE.COM|||||||||||||||||N|
PV1||N
OBX|1|NM|162986007^Pulso^SNM||72|^bpm|||||F|||20240402111716
OBX|2|NM|105723007^Temperatura^SNM||37|^Celsius|||||F|||20240402111716
OBX|3|NM|163030003^Presión sanguínea sistólica^SNM||142|^mmHg|||||F|||20240402111716
OBX|4|NM|163031004^Presión sanguínea diastólica^SNM||83|^mmHg|||||F|||20240402111716

前回の記事を覚えていらっしゃらない方のために説明すると、BPL は収縮期血圧が 140 を超えるか拡張期血圧が 90 を超えるとアラートが生成されるように定義していたため、このメッセージにより、DNI(スペインの身分証明書)が 07751332X である患者に対する警告タスクと、IRIS が新しい通知を受信するまで開いたままになる別の自動タスクが生成されます。

モバイルアプリケーションを確認しましょう。

2 つのタスクが生成されています。1 つは手動タスクとして定義されており、ユーザーによって終了方法が異なります。もう 1 つは自動タスクとして宣言されており、それを終了するには、ユーザーが血圧計で新しい測定を行う必要があります。 HTTP 呼び出しを見てみると、これにどのように JWT を含めているのかが分かります。

ユーザーが保留中のタスクの Accept ボタンをクリックすると、このフローは血圧計の測定値の受信を待機したままとなり、次のステップに進みません。 手動タスクが受け入れられ、血圧計からの新しい測定値を受信し、それがもう一度制限を超えている場合は、システムは患者向けのタスクと、関連する医師向けに危険な状態の可能性を警告するタスクの 2 つの新しい警告タスクを生成します。 :

完璧です! 患者通知システムは素早く簡単にセットアップ済みです。

まとめ

この連載記事でご覧になったとおり、Workflow Engine の機能と InterSystems IRIS の相互運用性機能を合わせることで、他のビジネスソリューションではほとんど実現できないビジネス プロセスの実装において、驚異的な可能性を切り開くことができます。 最大限に活用するためには、技術的な知識が必要となる可能性がありますが、実際に必要な取り組みを投じる価値はあります。

0
0 23
記事 Toshihiko Minamoto · 5月 22, 2025 6m read

しばらくの間、私はワークフロー機能について何らかの概念実証を行おうと計画していましたが、これは IRIS に存在する他の多くの機能と同様に、お客様にほとんど気付かれないまま終わってしまう傾向があります(その点については申し訳ありません)。 そこで数日前、この機能を構成して、Angular で開発したユーザーインターフェースに接続して使用するための例を作成することに決めました。

記事が非常に長くならなずに読みやすくするために、3 部に分けて説明しようと思います。 この最初の記事では、Workflow の機能とこれから解決する例について説明します。 2 つ目の記事では、Workflow の管理を担うプロファクションの構成と実装について詳しく説明します。 最後に、ウェブアプリケーションを通じて Workflow にある情報にアクセスする方法を説明します。

InterSystems IRIS Workflow Engine

この Workflow 機能を説明するには、IRIS ドキュメントに記載の説明をコピーするのが一番でしょう。

ワークフロー管理システムは、ユーザーへのタスクの分配を自動化します。 事前定義済みの戦略に従ってタスクの分配を自動化することによって、より効率的にタスクを割り当て、タスクの実行に対する責任感を高めることができます。 一般的な例は、顧客から問題に関する報告を受け取り、適切に対応できる組織のメンバーにその報告を送信し、問題が解決したら結果を顧客に報告するヘルプデスクアプリケーションです。

InterSystems IRIS Workflow Engine は、従来のスタンドアロン型のワークフロー管理システムよりもはるかに高レベルの機能を提供しています。

この Workflow 機能は IRIS のどこにあるのでしょうか? 非常に簡単です。管理ポータルからアクセスできます。

サンプルプロジェクトを詳しく見る前に、使用できる各オプションを簡単に説明します。

ワークフローロール

このオプションは、機能を使用するユーザーのロールを分類します。 ロールの定義に混乱するかもしれませんが、プロダクションから作成できるタスクのタイプとして考えています。 これらのロールに割り当てられる名前は、後でプロファクションで宣言し、そのロールに関連するタスクを作成するビジネスオペレーションの名前に一致する必要があります。

ワークフローユーザー

様々なタスクに割り当てられる IRIS ユーザーは、1 つ以上のロールに関連付けられます。 ユーザーは IRIS に登録されている必要があります。

ワークフロータスク

関連情報と共にシステムで作成されるタスクのリスト。 このウィンドウから、タスクのロールに応じた様々なユーザーにタスクを割り当てられます。

タスクは何でしょうか? 非常に簡単です。タスクは、EnsLib.Workflow.TaskRequest クラスのインスタンスであり、このタイプのオブジェクトは、生成されたビジネスプロセスから、クラス EnsLib.Workflow.Operation のビジネスオペレーションに、以前に作成したロールの名前とともに送信されます。 このアクションによって、EnsLib.Workflow.TaskResponse クラスのインスタンスが作成されます。 ビジネスオペレーションは、TaskResponse クラスインスタンスの CompleteTask メソッドが実行されるまで、レスポンスを返しません。

ワークフロー作業リスト

前の機能と同様に、関連する情報と共にタスクのリストも表示されます。

ワークフローの例

この記事に関連するプロジェクトには、慢性患者の治療など、医療組織の一般的な問題に対するソリューションを単純化した例が含まれます。 こういったタイプの患者には、継続的な管理と厳格なモニタリングが必要で、多くの場合、治療に関わる医療専門家にはそれらを行うための最適な手段がありません。

この例では、高血圧症の患者をモニタリングする方法を確認したいと思います。 患者は毎日、血圧計で血圧を測り、その血圧計から InterSystems IRIS がインストールされたサーバーにその読み取り値が送信されることを前提とします。 血圧測定値の収縮期血圧が 140、拡張期血圧が 90 を超える場合、その患者には特定の期間が過ぎてからもう一度血圧を測るように通知されます。2 回目の測定値がもう一度制限を超えた場合、患者と医師の両方にその状況が通知され、処置を決定することができます。

これを行うには、このタスクフローを 2 つのステージに分割します。

ステージ 1: 血圧測定値を毎日受信する。

  1. HL7 メッセージと血圧測定値をプロダクションで受信します。
  2. システムでユーザーの存在をチェックし(存在しない場合は作成)、その後で、ワークフローに関わるロールにユーザーを登録します。
  3. 血圧データを抽出し、アラート基準と比較します。
  4. データがアラート基準を超える場合:
    1. 状況を説明し、血圧をもう一度測定するように要求するための、患者向けのタスクを作成します。
    2. 2 回目の血圧測定値を管理するための自動タスクを作成します。
  5. データがアラート基準を超えない場合、このプロセスは次回の読み取りが翌日受信されるまで閉じられます。

ステージ 2: 最初の測定値がアラート値を超えた後の 2 回目の測定値を受信します。

  1. HL7 メッセージと 2 回目のデータ測定値を受信します。
  2. 自動タスクの復旧が保留されます。
  3. 血圧レベルがアラート基準を超える場合:
    1. 自動タスクは警告ステータスを示して中止されます。
    2. 患者の状況を報告する医師の手動タスクが作成されます。
    3. 状況と、医師に報告されたことを警告する患者の手動タスクが作成されます。
  4. レベルがアラート基準を超えない場合:
    1. 自動タスクは危険がないことを示して中止されます。

次の記事では、上記で説明した内容を反映するフローチャートのデザインについてより詳しく説明します。

ユーザーインターフェース

添付されたスクリーンショットからわかるように、InterSystems IRIS がタスク管理用に提供するユーザーインターフェースは控えめに言っても質素ではありますが、IRIS で得られるメリットの 1 つは、非常に簡単な方法でタスクフローを管理するための API REST を独自に作成できることです。この点については、最後の記事で説明します。

ユーザーに使いやすいインターフェースを提供するために、Angular Material を使用して、Angular で小さなウェブアプリケーションを開発しました。モバイルアプリケーションをシミュレーションするインターフェースを作成できます。

このモバイルアプリケーションは JSON Web Tokens を使って InterSystems IRIS に接続し、IRIS に公開された特定のウェブアプリケーションに HTTP リクエストを送信します。これにより、タスクに対してさまざまなアクターが実行したアクションが、定義されたタスクフローに反映されます。

次回の記事では...

この記事では Workflow の機能に関する紹介を終えたため、次の記事では、BPL を使って提供した例を解決するために必要なタスクフローを実装する方法を説明します。プロダクション全体を構成し、Workflow に関わるメインのクラス(EnsLib.Workflow.TaskRequestEnsLib.Workflow.TaskResponse など)を確認します。

お読みいただきありがとうございました!

0
0 36
記事 Toshihiko Minamoto · 5月 22, 2025 7m read

前回の記事では、一般的な概念と、InterSystems IRIS に統合されたタスクエンジンを使用して解決する問題を紹介しました。今回の記事では、相互運用性プロダクションを構成してソリューションを提供する方法を確認します。

Workflow Engine の構成

First we are going to define the roles of the tasks that we are going to manage, in our example we are going to define two types:

  • AutomaticBloodPressureRole: ユーザーの介入が不要な自動タスクを作成します。
  • ManualBloodPressureRole: ユーザーが手動で検証する必要のあるタスクを作成します。

後で様々な患者から HL7 メッセージを受信するたびにロールをユーザーに割り当てるため、ここでは割り当てる必要はありません。

また、IRIS ユーザーを Workflow に追加する必要もありません。これはプロダクションからコードで実行します。

プロダクションのセットアップ

この例では、アドホックで作成した「WORKFLOW」というネームスペース内にプロダクションを作成します。 このプロダクションに、必要とするビジネスコンポーネントを含めます。

最も単純なコンポーネントから説明し始めましょう。

ビジネスサービス

  • HL7_File_IN: 医療装置からのメッセージの受信をシミュレーションする特定のサーバーパスから HL7 ファイルを収集します。

ビジネスオペレーション

  • AutomaticBloodPressureRole: クラス EnsLib.Workflow.Operation のコンポーネント。Workflow で生成するために使用する、定義済みのロールの 1 つの名前を使用します。ユーザーとの直接的な対話を伴わないタスクです。
  • ManualBloodPressureRole: 前のビジネスオペレーションに似ていますが、この場合、生成するタスクには、それらをクローンするための直接的なユーザーの介入が必要です。

では、情報のフロー、およびサイクルに関連するタスクの作成と管理を管理するビジネスプロセスを詳しく見てみましょう。

Workflow.BP.BloodPressurePlan

このビジネスプロセスは BPL タイプで、患者のケアに関わるタスクの作成と、患者と血圧レベルに関連する情報を送信する医療装置からのレスポンスの取得を行います。

プロセスの開始:

この部分のフローは以下を行います。

  1. ユーザーのチェック: 受信した ADT^A08 メッセージのユーザーをチェックします。存在する場合はフローを続行し、存在しない場合は IRIS にユーザーを作成して、ワークフローに定義されたロールに割り当てます。 ユーザーをロールに割り当てるには、以下のコマンドを実行します。
    /// Workflow user creationset sc = ##class(EnsLib.Workflow.UserDefinition).CreateUser(username)
    /// Assigning Workflow user to the roleset sc = ##class(EnsLib.Workflow.RoleDefinition).AddUserToRole(role, username)
  2. 自動タスクの取得: このフローでは、自動タスクと手動タスクの両方を管理するため、受信した患者のユーザーに保留中の自動タスクがないかをチェックする必要があります。 保留中の自動タスクがある場合は、ノーマルレベルを超える値を持つ前の測定値があることを意味するため、重要です。 このチェックは、すべての作成されたタスクが保存されている TaskResponse テーブルで直接行います。クエリは以下のとおりです。
    &sql(SELECTIDINTO :taskId FROM EnsLib_Workflow.TaskResponse WHERE TaskStatus_AssignedTo = :username AND TaskStatus_IsComplete = 0AND %RoleName = :role)
  3. OBX 数の取得: HL7 メッセージから OBX セグメントの数を取得します。
  4. OBX 値の確認: OBX セグメントを確認し、血圧に関連するデータのみを抽出します。
  5. 保留中のタスクの有無: 2 番目のポイントで述べたように、保留中のタスクがあるかどうかを確認します。

1 回目の血圧測定:

この部分のフローでは、定義された最大血圧レベルを超える 1 回目の測定によって生成されていないメッセージを処理します。

  1. 血圧の確認: 血圧レベルが既定の制限を超えていないかをチェックします。超えていない場合は、プロセスは他の作業を行わずに終了します。 超えている場合は、櫃湯尾なタスクを作成する必要があります。
  2. 手動タスクの作成: 血圧が定義された安全レベルを超過している場合は、患者に状況を報告し、2 回目の測定が必要であることを示す手動タスクを作成する必要があります。 このアクティビティの構成を確認しましょう。    
    1. ビジネスオペレーション ManualBloodPressureRole を呼び出します。これにより、EnsLib.Workflow.TaskRequest タイプのメッセージを渡してタスクが生成されます。
    2. callrequest.%Actions に患者が実行できるアクションをカンマ区切りで定義します。ここでは、"Accept" アクションのみを定義しています。
    3. 患者に示すメッセージを callrequest.%Message に構成します。
    4. callrequest.%UserName でユーザー名を定義することで、作成したタスクに患者を割り当てます。
    5. 最後に、callequest.%Subject に題名またはタイトルを指定し、callrequest.%Priority に優先度(1~3)を指定します。
  3. 自動タスクの作成: 前のアクティビティの構成に似ていますが、ここではビジネスオペレーション AutomaticBloodPressureRole に送信します。 このタスクは、患者の 2 回目の血圧測定を待機するタスクで、保留中のタスクの有無のアクティビティで false を取得することによって、その患者に対してプロダクションで受信した新しいメッセージが新しいタスクを生成しないようにするタスクです。
  4. タスクの待機: このアクティビティは、タスクと手動操作が完了するまで、つまり、患者が ManualBloodPressureRole で作成されたアラームを閉じて IRIS プロダクションが医療装置から 2 回目のメッセージを受け取るまでタスクフローを停止します。
  5. 警告の有無のチェック: 前の保留中のタスクがクローズされたら、このアクティビティに進みます。 この時点では、自動タスクのクローズにおいて、構成された最大血圧値を超えるレベルの新しい測定を受信(タスクは警告アクションによってクローズされます)したかどうかを確認します。 測定値が最大値未満である場合、このプロセスは終了します。 測定値が最大値を超える場合は、次のアクティビティに進みます。
  6. 医師の警告タスク: 患者に割り当てられた医師に対し、患者が危険な状態にある可能性があることを報告する手動タスクが作成されます。
  7. 患者の警告タスク: 患者に対し、医師が状況が報告されたことを示す新しい手動タスクが作成されます。

2 回目の血圧測定:

この部分のフローは、前に作成された自動タスクが完了しなかった場合にのみ実行されます。

  1. 血圧のチェック: もう一度、血圧値が制限を超えるかどうかについて受信したメッセージを検証します。
  2. 通常タスクの終了: レベルが最大値を超えていない場合、タスクがインスタンス化する EnsLib.Workflow.TaskResponse クラスが提供する CompleteTask メソッドで保留中のタスクをクローズします。 使用されるパラメーターは対応処置を示しますが、この場合、FalseAlarm 値を渡して語法であることを示します。必要に応じて他の内容を定義することも可能です。
  3. 警告タスクの終了: 前の場合と同じですが、この場合に Workflow エンジンに渡すアクションは Warning であり、前の「警告の有無のチェック」アクティビティでチェックされる値です。

以下のダイアグラムで確認できるように(文字通りではありませんが、仕組みを把握することはできます)、タスクフローは 2 つの「スレッド」または「プロセス」で機能するようになっています。

最初のスレッド/プロセスは、2 回目の測定を待機する必要がある場合に備えてオープン状態のままとなり、2 回目の測定を受信したときに最初のオープンスレッドの再開を引き起こす、つまり定義されたタスクフローを終了するのは、2 つ目のスレッド/プロセスです。

まとめ

ご覧のように、BPL でタスクを構成するのは非常に単純です。それほど複雑な作業を行わずに、自動タスクと手動タスクをシミュレーションできます。 次の記事がこのシリーズの最後となりますが、ユーザーが外部アプリケーションからタスクを操作する方法を確認しましょう。

お読みいただきありがとうございました!

0
0 38
記事 Toshihiko Minamoto · 5月 16, 2025 5m read

Auth0 と InterSystems IRIS FHIR リポジトリ使った SMART On FHIR に関する連載最終回では、Angular 16 で開発したアプリケーションをレビューします。

このソリューションに定義されたアーキテクチャがどのように構成されているかを思い出しましょう。

フロントエンドのアプリケーションは 2 列目で、ご覧のように 2 つのことを行います。

  1. ログインリクエストを Auth0 にリダイレクトし、レスポンスを受信する。
  2. REST 経由でリクエストを FHIR サーバーに送信し、そのレスポンスを受信する。

Angular

Angular は TypeScript が開発し、Google が管理するオープンソースの Web アプリケーションフレームワークで、シングルページ Web アプリケーションの作成と管理に使用されます。 この「シングルページアプリケーション」デザイン手法によって、はるかに動的なユーザー向けアプリケーションを設計することができます。 最初の記事で説明したとおり、ここではアプリケーションサーバーとリバースプロキシとして NGINX を使用し、呼び出しヘッダーがサーバーの呼び出しヘッダーに一致するように変更して、CORS から派生する問題を回避します。

アプリケーションのデザイン

モバイルアプリケーションのデザインをシミュレートするように、Angular Material を使ってアプリケーションを設計しました。 この例では、アプリケーションは心拍数、血圧、および体重などの一連の患者データを記録することを目的としており、このために、2 種類の FHIR リソースをサーバーに送信します。1 つ目はユーザーがデータを登録する Patient タイプリソースで、2 つ目は、送信しようとしている各タイプのデータを送信する Observation リソースに対応します。

アプリケーションには、記録されたデータの変化がグラフで表示されます。

ログイン画面

ユーザーがパス https:\\localhost にアクセスすると最初の画面が表示され、そこからログインをリクエストできます。

 

ログインボタンをクリックすると、アプリケーションは構成済みの API に対して有効化された Auth0 ページに自動的にユーザーをリダイレクトします。

ユーザー名とパスワードを入力すると、Auth0 はデータへのアクセス許可をアプリケーションに付与するよう求めます。 データへのアクセスが確認されたら、Auth0 は、構成プロセス中に指定した URL にリダイレクトします。 アクセストークンが生成されると、Auth0 ライブラリは、サーバーに対して発行するすべての呼び出しのヘッダーにそのトークンを含めるようになります。 これは以下の図で確認できます。

最初の画面

ログインが完了すると、ログインユーザーが使用できる情報を FHIR サーバーにリクエストする最初の通信が発生します。これには、パラメーターによるクエリを使用して、次のような GET 呼び出しを送信します。

https://localhost:8443/smart/fhir/r5/Patient?email=lperezra%40intersystems.com

サーバーのレスポンスは次の情報を含む Bundle タイプリソースです。

{
    "resourceType":"Bundle",
    "id":"8c5b1efd-cfdd-11ee-a06b-0242ac190002",
    "type":"searchset",
    "timestamp":"2024-02-20T10:48:14Z",
    "total":0,
    "link":[
        {
            "relation":"self",
            "url":"https://localhost:8443/smart/fhir/r5/Patient?email=lperezra%40intersystems.com"
        }
    ]
}

ご覧のように、そのメールアドレスを使用する患者は合計 0 人であるため、アプリケーションにはデータを登録できる最初の画面が表示されます。

 

ご覧のように、メールアドレスのフィールドにはログインユーザーのアドレスが入力されています。これは、最初のクエリで見たように、メールアドレスを ID として使用するためです。 フォームの入力が完了したら、POST 経由で次のような呼び出しを送信します。

https://localhost:8443/smart/fhir/r5/Patient

Patient リソースによって形成されたメッセージ本文:

{
    "resourceType":"Patient",
    "birthDate":"1982-03-08",
    "gender":"male",
    "identifier":[
        {
            "type":{
                "text":"ID"
            },
            "value":"12345678A"
        }
    ],
    "name":[
        {
            "family":"PÉREZ RAMOS",
            "given":[
                "LUIS ÁNGEL"
                ]
        }
    ],
    "telecom":[
        {
            "system":"phone",
            "value":"600102030"
        },
        {
            "system":"email",
            "value":"lperezra@intersystems.com"
        }
    ]
}

患者データがサーバーに登録され、患者のクエリによって結果が返されるようになったため、さまざまな経過観察を記録できる準備が整いました。 最初の画面がどのように表示されるか見てみましょう。

経過観察画面

患者のデータを送信したのと同じ方法で、特定の画面から経過観察を送信します。

サーバーに送信されたリソースごとに、アプリケーションは新しい点をグラフに追加します。

これを行うために、そのユーザーに属する Observation タイプリソースをリクエストするクエリをサーバーに発行します。

https://localhost/smart/fhir/r5/Observation?patient=Patient/1

すると、サーバーはもう一度、その患者に対して記録されたすべての経過観察を含む Bundle タイプリソースを返します。

結果が取得されたので、アプリケーションはすべての数値を抽出し、関連するグラフを構築します。

まとめ

この記事と前回の 2 つの記事で確認したように、SMART On FHIR アプリケーションの設計と作成はそれほど複雑ではなく、FHIR サーバーで使用できるすべての機能を利用するアプリケーションを素早くアジャイルに構築することができます。

この種のアプリケーションでは、データに対する CRUD タイプの操作を管理する複雑なバックエンドの開発が不要であり、OAuth2 を使用することで、アプリケーションのユーザーを管理する必要もありません。その機能は Auth0 または選択した認証・承認サーバーに任せることができます。

SMART On FHIR では、簡単かつ単純な方法で、患者と医療専門家に対し医療データ管理に必要なツールを提供することができます。

お読みいただきありがとうございました!

0
0 40
記事 Toshihiko Minamoto · 12月 4, 2024 5m read

前回の記事では、ICD-10 による診断のコーディングをサポートできるように開発された d[IA]gnosis アプリケーションを紹介しました。 この記事では、InterSystems IRIS for Health が、事前トレーニングされた言語モデル、そのストレージ、およびその後の生成されたすべてのベクトルの類似性の検索を通じて ICD-10 コードのリストからベクトルを生成するために必要なツールをどのように提供するかを見ていきます。

はじめに

AI モデルの開発に伴って登場した主な機能の 1 つは、RAG(検索拡張生成)という、コンテキストをモデルに組み込むことで LLM モデルの結果を向上させることができる機能です。 この例では、コンテキストは ICD-10 診断のセットによって提供されており、これらを使用するには、まずこれらをベクトル化する必要があります。

診断リストをベクトル化するにはどうすればよいでしょうか?

SentenceTransformers と Embedded Python

ベクトルを生成するために、トレーニング済みのモデルからの自由テキストのベクトル化を大幅に容易にする SentenceTransformers という Python ライブラリを使用しました。 そのウェブサイトでは以下のように説明されています。

Sentence Transformers(別名: SBERT)は、最先端のテキスト画像埋め込みモデルへのアクセス、使用、およびトレーニングに使用される一般的な Python モジュールです。 SentenceTransformer モデルを使って埋め込みを計算(クイックスタート)するか、Cross-Encoder モデルを使って類似性スコアを計算(クイックスタート)するために使用できます。 これにより、セマンティック検索セマンティックテキスト類似性パラフレーズマイニングなどの広範なアプリケーションが可能になります。

SentenceTransformers コミュニティが開発した全モデルの中で、786 の次元ベクトルを生成する BioLORD-2023-M というトレーニング済みモデルを見つけました。

このモデルは、臨床文章や生物医学的概念の意味のある表現を生成するための新しい事前トレーニング戦略である BioLORD を使用してトレーニングされました。

最先端の方法は、同じ概念を指す名前の表現の類似性を最大化し、対照学習を通じて崩壊を防ぐことによって機能します。 ただし、生物医学的名前は必ずしも自明ではないため、非意味的な表現になることがあります。

BioLORD は、定義を使用した概念表現と、生物医学オントロジーで構成されるマルチリレーショナルナレッジグラフから得られる短い説明を基礎にすることで、この問題を克服しています。 この基礎により、このモデルは、オントロジーの階層構造により密接に一致する、より意味論的な概念表現を生成します。 BioLORD-2023 は、臨床文章(MedSTS)と生物医学概念(EHR-Rel-B)の両方でテキスト類似性の新たな最先端を確立しています。

この定義でわかるように、このモデルは、ICD-10 コードと自由テキストの両方をベクトル化するときに役立つ医療概念で事前トレーニングされています。

このプロジェクトでは、このモデルをダウンロードして、ベクトルの作成を高速化します。

if not os.path.isdir('/shared/model/'):
    model = sentence_transformers.SentenceTransformer('FremyCompany/BioLORD-2023-M')            
    model.save('/shared/model/')

ダウンロードしたら、ベクトル化するテキストをリストに入力して、プロセスを高速化します。以前に ENCODER.Object.Codes クラスに記録した ICD-10 コードをベクトル化する方法を見てみましょう。

st = iris.sql.prepare("SELECT TOP 50 CodeId, Description FROM ENCODER_Object.Codes WHERE VectorDescription is null ORDER BY ID ASC ")
resultSet = st.execute()
df = resultSet.dataframe()

if (df.size > 0): model = sentence_transformers.SentenceTransformer("/shared/model/") embeddings = model.encode(df['description'].tolist(), normalize_embeddings=True)

df[<span class="hljs-string">'vectordescription'</span>] = embeddings.tolist()

stmt = iris.sql.prepare(<span class="hljs-string">"UPDATE ENCODER_Object.Codes SET VectorDescription = TO_VECTOR(?,DECIMAL) WHERE CodeId = ?"</span>)
<span class="hljs-keyword">for</span> index, row <span class="hljs-keyword">in</span> df.iterrows():
    rs = stmt.execute(str(row[<span class="hljs-string">'vectordescription'</span>]), row[<span class="hljs-string">'codeid'</span>])

else: flagLoop = False

ご覧のとおり、CSV ファイルから抽出した後に前のステップで登録した、まだベクトル化されていないコードを先に抽出し、次に、ベクトル化する記述のリストを抽出します。Python の sentence_transformers ライブラリを使用してモデルを復元し、関連する埋め込みを生成します。

最後に、UPDATE を実行して、ベクトル化された記述で ICD-10 コードを更新します。 ご覧のように、モデルが返した結果をベクトル化するコマンドは、IRIS における SQL コマンドの TO_VECTOR です。

IRIS で使用する

Python コードの準備ができたので、Ens.BusinessProcess を拡張するクラスにラップして、プロダクションに含めましょう。次に、CSV ファイルを取得するビジネスサービスに接続すれば、完成です!

プロダクションでこのコードがどのように見えるかを確認しましょう。

ご覧のように、EnsLib.File.InboundAdapter アダプターを備えたビジネス サービスにより、コードファイルを収集し、それをすべてのベクトル化とストレージ操作を実行するビジネスプロセスにリダイレクトできます。すると、以下のようなレコードセットが得られます。

これで、アプリケーションは、送信するテキストに一致する可能性のある項目を検索できるようになりました!

次回の記事では...

次回の記事では、Angular 17 で開発されたアプリケーションのフロントエンドが IRIS for Health のプロダクションとどのように統合されるか、および IRIS が分析するテキストをどのように受け取り、それらをベクトル化し、ICD-10 コードテーブルで類似性を検索するかを説明します。

お見逃しなく!

0
0 83
記事 Toshihiko Minamoto · 11月 26, 2024 5m read

ベクトルデータ型と Vector Search 機能が IRIS に導入されたことにより、アプリケーションの開発に多数の可能性が開かれました。こういったアプリケーションの例として、バレンシア保健省が AI モデルを使用した ICD-10 コーディング支援ツールを要求した公募で出品されたアプリケーションが最近私の目に留まりました。

要求されたツールのようなアプリケーションをどのように実装できるでしょうか? 必要なものを確認しましょう。

  1. ICD-10 コードのリスト。自由テキスト内で診断を検索するための RAG アプリケーションのコンテキストとして使用します。
  2. ICD-10 コード内で相当するものを検索するためにテキストをベクトル化するトレーニング済みモデル。
  3. ICD-10 コードとテキストの取り込みとベクトル化を行うために必要な Python ライブラリ。
  4. 可能性のある診断を見つけるためのテキストを受け入れる使いやすいフロントエンド。
  5. フロントエンドから受信するリクエストのオーケストレーション。

これらのニーズに対応するために、IRIS は何を提供できるでしょうか?

  1. CSV インポート。RecordMapper 機能を使うか、Embedded Python を直接使用します。
  2. Embedded Python によって、選択されたモデルを使ってベクトルを生成するために必要な Python コードを実装できます。
  3. フロントエンドアプリケーションから呼び出される REST API の公開。
  4. IRIS 内で情報を追跡できる相互運用性プロダクション。

では、開発済みの例を確認しましょう。

d[IA]gnosis

この記事に関連して、開発済みのアプリケーションにアクセスできます。次の記事では、モデルの使用、ベクトルのストレージ、ベクトル検索の使用に至るまで、各機能をどのように実装するかを詳しく説明します。

アプリケーションを確認しましょう。

ICD-10 コードをインポートする

構成画面に、CSV ファイルのフォーマットはインポートしようとしている ICD-10 コードに準拠している必要があると表示されます。 読み取りとベクトル化のプロセスは多数のリソースを使用し、長時間かかるため、Docker コンテナーのデプロイによって、Docker が使用できる RAM メモリだけでなく、要件が割り当てられた RAM を超過する場合に備えてディスクメモリも構成されます。

# iris  iris:    init:true    container_name:iris    build:      context:.      dockerfile:iris/Dockerfile    ports:      -52774:52773      -51774:1972    volumes:    -./shared:/shared    environment:    -ISC_DATA_DIRECTORY=/shared/durable    command:--check-capsfalse--ISCAgentfalse    mem_limit:30G    memswap_limit:32G

ICD-10 コードを含むファイルは、プロジェクトパス /shared/cie10/icd10.csv にあります。100% に達すスト、アプリケーションを使用できます。

私たちのアプリケーションでは、診断コーディング用に 2 つの異なる機能を定義しました。1 つはシステムで受信した HL7 メッセージに基づいたもので、もう 1 つは自由テキストに基づいたものです。

HL7 による診断のキャプチャ

プロジェクトにはテスト用に準備された HL7 メッセージが含まれており、/shared/hl7/messagesa01_en.hl7 ファイルを /shared/HL7In フォルダにコピーすれば、関連付けられたプロダクションが診断を抽出してウェブアプリケーションに表示します。

診断リクエスト画面では、HL7 メッセージングで受信したすべての診断を確認できます。 これを ICD-10 にコーディングするには、虫眼鏡をクリックするだけで、受信した診断に最も近い ICD-10 コードのリストを表示することができます。

選択すると、リストに診断とそれに関連付けられた ICD-10 コードが表示されます。 封筒のアイコンが付いたボタンをクリックすると、元のメッセージと、診断セグメント内で選択した新しいメッセージによって、メッセージが生成されます。

MSH|^~\&|HIS|HULP|EMPI||||ADT^A08|592956|P|2.5.1
EVN|A01|
PID|||1556655212^^^SERMAS^SN~922210^^^HULP^PI||GARCÍA PÉREZ^JUAN^^^||20150403|M|||PASEO PEDRO ÁLVAREZ 1951 CENTRO^^LEGANÉS^MADRID^28379^SPAIN||555283055^PRN^^JUAN.GARCIA@YAHOO.COM|||||||||||||||||N|
PV1||N
DG1|1||O10.91^Unspecified pre-existing hypertension complicating pregnancy^CIE10-ES|Gestational hypertension||A||

このメッセージは /shared/HL7Out パスにあります。

自由テキストによる診断のキャプチャ

テキストアナライザーオプションでは、分析プロセスが実行される自由テキストを含めることができます。 アプリケーションは、見出し語化された 3 つの単語のタプルを検索します(冠詞、代名詞、およびその他の関連性の低い単語は削除されます)。 分析が完了すると、システムは関連する下線付きのテキストと、考えられる診断を表示します。

分析の実行が完了したら、その内容は分析履歴からいつでも参照できます。

分析履歴

実行されたすべての分析は記録され、いつでも参照でき、利用可能なすべての ICD-10 コードを表示できます。

次回の記事では...

Embedded Python を使用して、コンテキストとして使用する ICD-10 コードと自由テキストの両方のベクトル化に特定の LLM モデルを使用する方法を見ていきます。

ご質問やご提案がありましたら、ぜひこの記事のコメントでお知らせください。

0
0 63
記事 Toshihiko Minamoto · 8月 6, 2024 7m read

前回の記事でSMART On FHIRプロジェクトのアーキテクチャを紹介したので、いよいよ本題に入り、必要となる全ての要素の設定を始めましょう。

まずはAuth0から始めます。

Auth0の設定

登録が完了したら、左側のメニューから最初のアプリケーションを作成します

Application menu

この例では、Angular 16で開発されたアプリケーションなので、Single Page Web Applicationタイプとなります。このオプションを選択し、Createをクリックします。

Single Page Web Application

次の画面では以下のフィールドを定義する必要があります。

注意: URLはすべてHTTPSでなければなりません。これはOAuth2接続要件の1つです。

これはOAuth2接続の要件の1つです。これでAuth0が認証と認可プロセスの後にユーザーをリダイレクトするために必要なURLを設定しました。このURLにはポートが定義されていませんが、これはNGINXを通してAngularプロジェクトをDockerにデプロイする際に、デフォルトのHTTPSポート443からアクセスするように指定したためです。好きな名前を付けてください。

Application configuration

この後のAngularプロジェクトの設定について、DomainClient IDの両方にある値を書き出します。

アプリケーションの設定が完了したら、次はAngularアプリケーションからのリクエストを受け取るAPIを定義します。

このオプションを選択すると、必要なデータを入力する画面が表示されます。

API configuration

後でAngularアプリケーションが適切に接続するための "環境 "として使用するAPIの識別子を定義する必要があります。見ての通り、URLを入力することが推奨されていますが、このURLは識別子として使用されるだけなので、実際に機能する必要はありません。この場合、以下のように定義できます。

https://localhost/smart/fhir/r5

最後に、署名アルゴリズムをRS256に設定し、接続ユーザーのFHIRのスコープを定義するPermissionsタブに進みます

API permission

FHIRコンテキストのトピックをより深く知りたい場合は、[ここ](http://hl7.org/fhir/smart-app-launch/1.0.0/scopes-and-launch-context/index.html)をクリックして公式ページのURLを参照してください。この例では、user/*.*スコープを定義し、認証されたユーザーがすべての FHIR サーバーリソースに対して CRUD 操作を実行できるようにしています。

完璧です!Auth0アカウントがOAuth2サーバとして機能するように設定できました。

Angularアプリの設定

このアプリケーションはAngular 17で開発したかったのですが、残念ながらAuth0とそのライブラリの関連ドキュメントはAngular 16のものしかないので、簡単な方を選ぶこととしバージョン16で開発しました。

Angularプロジェクトを設定するには、app.module.tsページを開き、以下のコードを探します

設定する各パラメータの意味を見てみましょう。

  • domain:Auth0 でアプリケーションを作成したときに生成された ドメイン値
  • clientId: 上と同じく生成されたClient ID
  • audience: APIの識別子として設定したURL
  • uri: Auth0 TypeScriptライブラリに、そのURIを含むURLへのすべての呼び出しをインターセプトし、Auth0が検証時に返すAccess_tokenを組み込むように指示します(呼び出しのヘッダにAuthorizationパラメータを追加します: Bearer...)

これらの値を変更すると、AngularアプリケーションがAuth0で動作するように設定されます。次の記事では、ユーザーインターフェースからAuth0を呼び出す方法を詳しく説明します。

InterSystems IRIS for Healthの設定

このプロジェクトは、デプロイプロセス中にFHIRサーバーを自動的にインストールするように設定されているため、手順を省くことができます。今回のケースでは、FHIRサーバーのエンドポイントとして/smart/fhir/r5というURIを定義しました。FHIRの用語に馴染みのない方のために説明すると、r5とはFHIRの最新バージョンのことで、IRISで数ヶ月前から利用可能です。

IRISインスタンスを設定するには、まずDockerを起動し、プロジェクトで利用可能な3つのコンテナをデプロイする必要があります。プロジェクトのルートからターミナルで以下のコマンドを実行するだけです。

docker-compose up -d --build

これで、プロジェクトに存在するコンテナをビルドして実行できるようになります。Windowsユーザーの場合、Docker Desktopを使えば、このような画面が表示されるはずです。

Docker Desktop

ここでは3つのコンテナがあります。

  • iris: FHIRサーバが配置されているIRISインスタンス
  • smart-ui: NGINXからデプロイされたウェブアプリケーションのコードで、すべての接続が関連する証明書とSSLを経由するよう設定されています。
  • webgateway: 関連するApacheサーバー(正式版2023.1以降、プライベートWebサーバーは廃止されましたが、コミュニティ版ではまだ利用可能であることを覚えておいてください)。

ウェブゲートウェイ

繰り返しになりますが、OAuth2をFHIRサーバーで使用するためには、すべての接続がHTTPS経由で行われることが必須であるため、Apacheサーバーはそのタイプの呼び出しのみを受け付けるように設定する必要があります。 webgateway/shared/CSP.confファイルをみると、Apacheサーバの設定を担当する以下のセクションがあります

# SSL SECTION #
# Enable SSL/TLS (https://) on the Apache web server.
# The user is responsible for providing valid SSL certificates.
LoadModule ssl_module /usr/lib/apache2/modules/mod_ssl.so
LoadModule headers_module /usr/lib/apache2/modules/mod_headers.so
<VirtualHost *:443>
SSLEngine on
SSLCertificateFile "/webgateway-shared/apache_webgateway.cer"
SSLCertificateKeyFile "/webgateway-shared/apache_webgateway.key"
Header add ACCESS-CONTROL-ALLOW-ORIGIN "*"
</VirtualHost>

443番ポートを経由するようにし、WebGatewayのURLはhttps://webgateway、アプリケーションからFHIRサーバへの呼び出しはこのURLにリダイレクトする必要があります。(ウェブゲートウェイは、Dockerコンテナによって作成されたネットワークに与えられたマスクです)

Angularからサーバーへのすべての呼び出しはURLhttps://localhost/smart/fhir/r5で行われ、NGINXはそのlocalhostをwebgatewayにリダイレクトする役割を果たします。smart-ui/nginx.confを開くと、以下のような設定になっています。

 

この設定では、Webアプリケーションは443番ポートをリッスンし、URLに/を含むすべての呼び出しはAngularアプリケーションによって管理され、/smart/を含む呼び出しはすべてhttps://webgatewayにリダイレクトされます。 CORSによる頭痛の種を回避するproxy_set_headerには注意が必要です。Web Gatewayが他のサーバーからの呼び出しを拒否しないようにするには、Host ヘッダーの値を変更して、Web Gatewayのアドレスで設定する必要があります。

InterSystems IRIS

次に IRIS を Auth0 で動作するように設定する必要がありますが、そのためには IRIS を OAuth2 クライアントとして設定する必要があります。これを行うには、スーパーユーザ/SYS認証情報で管理ポータルにアクセスし、[System Administration (システム管理)] > [Security (セキュリティ)] > [OAuth 2.0 (OAuth 2.0)] > [Client (クライアント)] オプションにアクセスし、[Create Server Description (サーバ記述の作成)] をクリックし、発行者エンドポイントに Auth0 へのアプリケーション作成時に取得したドメイン値 (https://[MY_DOMAIN]/) を入力します。注意してください!URLは"/"で終わっていなければなりません。最後にSSL/TLSコンフィギュレーションを選択し、Discover and Saveをクリックします。

IRIS client

IRISインスタンスは自動的にAuth0から必要な情報を取得します

Issuer endpoint

先ほど設定したサーバにクライアントを追加するだけです。Client Configuration を押すと、アプリケーションとクライアントの名前を定義する新しい画面にアクセスできます。このクライアント名は、後で FHIR サーバを設定する際に使用します。

FHIRサーバ

プロジェクトの設定を完了する最後のステップは、接続に使用するOAuth2クライアントをFHIRサーバに伝えることです。コンフィギュレーションにアクセスするには、管理ポータルを開き、Health > FHIR > FHIR Configuration > Server Configurationを選択して、画面に表示されているエンドポイントを開きます。最後にOAuth Client Nameフィールドにクライアント・コンフィギュレーションを作成した名前を追加します。

FHIR OAuth Configuration

まとめ

さて、プロジェクトの設定は完了しました。次回は、Angularアプリケーションが各アクターとどのように相互運用するかを見ていきましょう。

ご清聴ありがとうございました!

0
0 74
記事 Toshihiko Minamoto · 6月 12, 2024 4m read

はじめに

先日、@Patrick Jamieson が開催した素晴らしいハンズオンに参加してきました。そこでは、SMART On FHIR が定義したプロトコルに従って Angular アプリケーションを IRIS FHIR サーバーとともに構成したセッションでした。非常に深い興味を得たため、独自に Andular アプリケーションを開発し、そこから学んだことを活用してコミュニティに共有することにしました。

SMART On FHIR

SMART On FHIR について Google ではどのように説明しているのか見てみましょう。

SMART On FHIR は電子健康記録(EHR)システムの情報にアプリケーションがアクセスできるようにするためのデータ規格です。 アプリケーション開発者は、その規格を採用した EHR システムに接続する単一のアプリケーションを作成できます。

SMART On FHIR では、以下の主な概念を処理します。

  • OAuth2 または OpenID による代理認証と承認。
  • 定義されたコンテキストにおける FHIR リソースの操作。
  • HTTPS 通信。

プロジェクトのアーキテクチャ

この演習では、Docker と Auth0 サービスの両方で以下の要素を構成しました。

  • フロントエンドとして機能する Angular で開発されたアプリケーション。このアプリケーションは SMART On FHIR の原則に従って開発されています。
  • Angular で開発されたアプリケーションを公開する NGINX Web サーバーおよびリバースプロキシ。
  • Auth0 が提供する OAuth2 による認証および承認サービス。
  • FHIR サーバーをデプロイし、Docker イメージですでに提供されている Apache サーバーを含む Web ゲートウェイを通じて接続する InterSystems IRIS。

Auth0

ユーザーの認証と承認をこの目的でデプロイされた別の IRIS サーバーに委任することもできますが、ここでは、Auth0 が提供するサービスを使用することにします。

Auth0 とは?

Auth0 は、プラットフォームの認証と承認を処理するメカニズム全体を提供するサービスです。

Auth0 には、あらゆるプロジェクトに簡単に統合できる多様な言語に特化したライブラリもあるため、SMART On FHIR に基づく開発において必ず考慮できるオプションです。

アプリケーションでの Auth0 の使用

OAuth2 の使用は SMART On FHIR を使用するための必要要件であるため、認証、承認、およびアプリケーションアクセスの通常のプロセスに OAuth2 サーバーを含める必要があります。 以下の図は、Auth0 サービスを使ってシステムに送信される情報が辿るパスを示しています。

では、プロセスを分析しましょう。

  • ログインリクエスト:
    1. ログインリクエスト: ユーザーがインターネットブラウザを使用してアプリケーションにアクセスし、ログインをリクエストします。
    2. ログインリクエスト: Angular アプリケーションがそのリクエストを Auth0 サービスに転送します。
    3. ログインページ: Auth0 がユーザーのインターネットブラウザを開発者が作成したページにリダイレクトします。
  • Auth0 での認証:
    1. ユーザー認証情報: ユーザーが、Auth0 に登録されたメールアドレスとパスワードを入力します。
    2. 認証と承認: Auth0 がデータを検証し、ユーザーに割り当てられたコンテキストを含む Access_token を生成します。
    3. Access_token のレスポンスとリダイレクト: Auth0 が生成されたトークンを含むレスポンスをプロジェクト構成に指定された URL にリダイレクトします。
    4. 患者の画面: Angulaar アプリケーションが、個人情報を登録するページをユーザーに表示します。
  • FHIR リソースレコード:
    1. 患者の保存: ユーザーが個人情報をフォームに入力し、Angular アプリケーションがフォームを FHIR Patient リソースのフォーマットで JSON オブジェクトに変換します。
    2. POST リクエスト: Angular アプリケーションが IRIS にデプロイされている FHIR サーバーに、認証トークンとしての access_token をリクエストヘッダーに含めて HTTP POST 呼び出しを送信します。
    3. POST レスポンス: Web ゲートウェイを通じて POST リクエストを受け取ると、IRIS はトークンの有効性とリクエストのコンテキストをチェックします。 すべてが正しければ、受信したリソースを検証して FHIR サーバーに登録し、新しいリソースの作成を示す HTTP 201 を返します。 また、ヘッダーに、新しいリソースに割り当てられた識別子が含められます。
    4. 処理成功: Angular アプリケーションが、主な機能を示す画面にユーザーをリダイレクトします。

ログインが完了すると、プロジェクトに含まれる Auth0 ライブラリが、FHIR サーバーに対して発行するすべてのリクエストを傍受し、Auth0 から受け取るアクセストークンを含めます。

今後の予定

次の記事では、関りのある各システムを構成し、最終的に Angular アプリケーションに接続する方法を説明します。 記事の公開を待ちきれない方は、この記事にリンクされた OpenExchange プロジェクトに関連付する GitHub で README.md をお読みください。Auth0 と InterSystems IRIS の両方を構成する方法が詳しく説明されています。

Awfully Good: Stay Tuned (1992) with John Ritter

0
0 127
記事 Toshihiko Minamoto · 9月 15, 2023 5m read

DeepSee BI ソリューションのユーザーインターフェース(UI)を配布するにはいくつかのオプションがあります。 最も一般的には以下の手法があります。

  • ネイティブの DeepSee ダッシュボードを使用し、Zen で Web UI を取得して、Web アプリに配布する。
  • DeepSee REST API を使用して、独自の UI ウィジェットとダッシュボードを取得・構築する。

最初の手法はコーディングを行わずに比較的素早く BI ダッシュボードを構築できるためお勧めですが、事前設定のウィジェットライブラリに限られます。これを拡張することはできますが、大きな開発の手間がかかります。

2 つ目の手法には、任意の総合 js フレームワーク(D3,Highcharts など)を使用して DeepSee データを可視化する手段がありますが、ウィジェットとダッシュボードを独自にコーディングする必要があります。

今日は、上の 2 つを組み合わせて Angular ベースの DeepSee ダッシュボード用 Web UI を提供するもう 1 つの手法をご紹介します。DeepSee Web ライブラリです。

詳しい説明

DeepSee Web(DSW)は、DeepSee ダッシュボードを表示する Angular.js Web アプリで、Highcharts.js、OpenStreetMap、および一部の自作 js ウィジェットを使用する特定のネームスペースで使用できます。

仕組み

DSW は、MDX2JSON ライブラリを使用するネームスペースで使用できるダッシュボードとウィジェットのメタデータをリクエストします。 ダッシュボードを可視化するために、DSW はウィジェットとそのタイプのリストを読み取り、ほぼすべての DeepSee ウィジェットに実装されている js-widget アナログを使用します。 ダッシュボードにリストされる js ウィジェットをインスタンス化し、DeepSee REST API と MDX2JSON を使用してウィジェットのデータソースに従ってデータをリクエストし、JSON 形式でデータを取得して可視化を実行します。

そのメリットは?

DSW は以下の理由により優れています。

  • 標準のエディターを使用して DeepSee ダッシュボードを作成し、コーディングを一切行わずに Angular で UI を使用できます。
  • 任意の js ライブラリまたは自作ウィジェットで簡単にウィジェットのライブラリを拡張できます。
  • DSW カバーにスマートタイルを追加できます。
  • 地図にデータを表示できます。

モバイルデバイスは?

DSW は、モバイルブラウザ(Safari、Chrome)で非常によく動作し、スタンドアロンの DeepSight iOS アプリもあります。以下はそのスクリーンショットです。

いくらですか?

無料です。 最新リリースのダウンロード、課題の送信、フォークの作成、修正や新機能を含むプルリクエストを自由に行えます。

プロダクションでの使用に適していますか?

はい。 DSW は 2014 年にリリースされ、今日まで 60 回リリースされてきました。 DSW は数十社の DeepSee ソリューションで正常に使用されています。

たとえば、開発者コミュニティでも DSW を使用しています。

ただし、DSW ライセンス(MIT ライセンス)に従い、ご自身のリスクで使用していただきます。

 

サポートされていますか?

コミュニティによってサポートされています。 ぜひ課題を開き、フォークを作成して、修正と新機能が含まれるプルリクエストを送信してください。

主要貢献者は [@Anton Gnibeda​]、[@Nikita Savchenko]、[@Eduard Lebedyuk] です。 

インストール方法は?

簡単です。 まず、MDX2JSON をインストールします。 リリースから最新のインストーラーをダウンロードして、USER ネームスペースで installer.xml をインポート/コンパイルし、以下を実行します。

USER> D ##class(MDX2JSON.Installer).setup()

GitHub から最新のリリースがダウンロードされ、MDX2JSON データベースとネームスペースの作成と %All へのマッピングが行われ、/MDX2JSON Web アプリが作成されます。

すべてがうまくインストールされたことを確認するには、localhost:57772/MDX2JSON/Test を開きます。  すべてが正常である場合は、以下のように返されます。

{
"DefaultApp":"\/dsw",
"Mappings": {
"Mapped":["%SYS","MDX2JSON","SAMPLES","USER"
],
"Unmapped":["DOCBOOK"
]
},
"Parent":"18872dc518d8009bdc105425f541953d2c9d461e",
"ParentTS":"2017-05-26 16:20:17.307",
"Status":"OK",
"User":"",
"Version":2.2
}​

 

次に DSW をインストールします。 最新の release xml をダウンロードします。

それを USER ネームスペースにインポート/コンパイルします。 以下を実行してください。

USER> Do ##class(DSW.Installer).setup()

/dsw アプリが作成され、すべての js ファイルが /csp/dsw フォルダにインストールされます。

localhost:57772/dsw/index.html#!/?ns=SAMPLES を開き、動作することを確認してください。

既知の問題:

一部のシステムでは、CSP ゲートウェイで CSP 以外のファイルに UTF8 サポートを設定する必要があります。 ターミナルで以下を実行してオンにしてください。

USER> set ^%SYS("CSP","DefaultFileCharset")="utf-8"

 

いつインストールすべきですか?

今すぐインストールしましょう! )

続きにご期待ください!

0
0 177
記事 Mihoko Iijima · 4月 6, 2023 1m read

開発者の皆さん、こんにちは!

この記事では、Developer Hub にあるチュートリアルの4番目のご紹介となる REST + Augular App チュートリアル についてご紹介します。

チュートリアルでは、IRISを利用してRESTサービスで使用するテーブル、データ、RESTサーバの機能をサンプルコードをコピーしながら作成していきます。

チュートリアルの中で作成するWebアプリはとてもシンプルな内容で、データベースにブックマークとして登録したいURLを保存し、参照するだけのページとしています。

データの登録も、IRISの管理ポータルのSQLメニューでINSERT文を実行する形式で進めていきます。

最終的には、以下の図にあるように「Add a new bookmark」以下のテキストボックスで新しいブックマークを登録できるように、Web画面とRESTサーバ用コードを追加していきます。

アカウント作成やログインも不要で  ボタンをクリックするだけで始められます👍

ぜひ、お試しください!​​​​​​

0
1 259
記事 Toshihiko Minamoto · 12月 12, 2022 16m read

こんにちは! 今日は、Angular で最も重要なアーキテクチャパターンの 1 つについてお話しします。

パターン自体は直接 Angular に関連していませんが、Angular はコンポーネント駆動のフレームワークであるため、このパターンは最新の Angular アプリケーションを構築するために最も不可欠なものの 1 つです。

「コンテナ・プレゼンテーション」パターン

コンポーネントは、小さい、目的集中型、独立型、テスト可能性、そして一番重要と言える再利用可能性という特性が備わっているのが最適だと考えられています。

コンポーネントに、サーバー呼び出しを行う、ビジネスロジックが含まれている、他のコンポーネントと密に連携している、他のコンポーネントまたはサービスの内部を過度に知っている、という特徴が備わっていれば、より大きく、テスト、拡張、再利用、変更が行いにくいものになってしまします。 「コンテナ・プレゼンテーション」パターンは、これらの問題を解決するために存在します。

一般的にすべてのコンポーネントは、コンテナコンポーネント(スマート)とプレゼンテーションコンポーネント(ダム)の 2 つのグループに分けられます。

コンテナコンポーネントは、サービスからデータを取得し(ただし、サーバー API を直接呼び出さない)、何らかのビジネスロジックを含み、サービスまたは子コンポーネントにデータを配信することができます。 通常、コンテナコンポーネントは、ルーティング構成でルートが指定されたコンポーネントとして指定されるものです(もちろん、必ずしもそうとは限りません)。

プレゼンテーションコンポーネントは、入力としてデータを取り、何らかの方法で画面に表示することのみを行います。 これらのコンポーネントはユーザー入力に反応しますが、それはローカルで独立した状態を変更することでのみ発生するものです。 アプリの残りの部分とのすべてのやり取りは、カスタムイベントを発行することで行われます。 これらのコンポーネントには非常に高い再利用可能性が備わっている必要があります。

もう少し分かりやすいように、コンテナコンポーネントとプレゼンテーションコンポーネントの例をいくつか挙げましょう。

コンテナ: About ページ、ユーザーページ、管理者パネル、注文ページなど。

プレゼンテーション: ボタン、カレンダー、テーブル、モーダルダイアログ、テーブルビューなど。

めちゃくちゃなボタンの例

コンポーネントアプローチが誤用されている例として、実際のプロジェクトにおいて私自身が体験した非常に悪い例を見てみましょう。

@Component({
  selector: 'app-button',
  template: `<button class="className" (click)=onClick()>{{label}}</button>`
})
export class ButtonComponent {
  @Input() action = '';
  @Input() className = '';
  @Input() label = '';

  constructor(
    private router: Router,
    private orderService: OrderService,
    private scanService: ScanService,
    private userService: UserService
  ) {}

  onClick() {
    if (this.action === 'registerUser') {
      const userFormData = this.userService.form.value;
      // some validation of user data
      // ...
      this.userService.registerUser(userFormData);
    } else if (this.action === 'scanDocument') {
      this.scanService.scanDocuments();
    } else if (this.action === 'placeOrder') {
      const orderForm = this.orderService.form.values;
      // some validation and business logic related to order form
      // ...
      this.orderService.placeOrder(orderForm);
    } else if (this.action === 'gotoUserAccount') {
      this.router.navigate('user-account');
    } // else if ...
  }
}

読みやすいように単純化してありますが、実際にはもっとひどいものです。 これは、ユーザーがクリックして呼び出せるすべての可能なアクションが含まれたボタンコンポーネントです。API 呼び出し、フォームの検証、サービスからの情報取得などを行います。 このようなコンポーネントは、比較的小さなアプリケーションで使用されていたとしても、あっと言う間に地獄入りしてしまうのが想像できるでしょう。 私が見つけて(後でリファクタリングした)このようなボタンコンポーネントのコードは、2000 行以上にも及ぶものでした。 あり得ません!

それを書いた開発者に、すべてのロジックを 1 つのコンポーネント収めた理由を尋ねたところ、本人が行ったのは「カプセル化」だという答えが返ってきました。🙀

理想的なコンポーネントに備わっているべき特性について思い出しましょう。

小さい - 2000 行以上のコードでできたこのボタンは小さくありません。 また、他のアクション用に別のボタンが必要という人が現れるたびに、コンポーネントが膨れ上がってしまうでしょう。

目的集中型 - このボタンはまったく無関係な多くのアクションを実行するため、目的集中型とは呼べません。

独立型 - このボタンはいくつかのサービスとフォームと密に連携しているため、いずれかを変更すると、ボタンに影響があります。

テスト可能性 - ノーコメントです。

再利用可能性 - まったく再利用できません。 使用するたびに、含まれていないアクションを追加するためにコンポーネントのコードを変更する必要があります。さらに、このボタンの不要なアクションや依存関係もすべて含まれることになります。

さらに、このボタンコンポーネントは、ネイティブの HTML ボタンを内部的に非表示にするため、開発者はそのプロパティにアクセスできません。 これが、コンポーネントの悪い書き方としての良い例です。

このボタンコンポーネントを使用する非常に単純なコンポーネントの例を示し、コンテナ・プレゼンテーションパターンでリファクタリングしてみましょう。

@Component({
  selector: 'app-registration-form',
  template: `<form [formGroup]="userService.form">
  <input type="text" [formControl]="userService.form.get('username')" placeholder="Nickname">
  <input type="password" [formControl]="userService.form.get('password')" placeholder="Password">
  <input type="password" [formControl]="userService.form.get('passwordConfirm')" placeholder="Confirm password">
  <app-button className="button accent" label="Register" action="registerUser"></app-button>
</form>
`
})
export class RegistrationFormComponent {
  constructor(public userService: UserService) {}
}

このコンポーネントの中には何もロジックがありません。フォームはサービスに格納されており、ボタンにはクリックによるすべてのロジックの呼び出しが含まれます。 現時点では、ボタンにその動作に関係のないすべてのロジックが含まれており、ボタンが処理するアクションに直接関係している親コンポーネントよりスマートです。

コンテナ・プレゼンテーションパターンでボタンコンポーネントをリファクタリングする

これらの 2 つのコンポーネントの関数を分離しましょう。 ボタンはプレゼンテーションコンポーネント、つまり小さく再利用可能である必要があります。 このボタンを含む登録フォームは、ビジネスロジックやサーバーレイヤーとのやり取りを保持するコンテナコンポーネントとすることができます。

ネイティブボタンを表示する部分は説明せずに(これについては、おそらく今後の記事で話すことにします)、主に 2 つのコンポーネントのアーキテクチャ上の関係に焦点を当てます。

リファクタリングしたボタンコンポーネント(プレゼンテーション)

@Component({
  selector: 'app-button',
  template: `<button class="className" (click)=onClick()>{{label}}</button>`
})
export class ButtonComponent {
  @Input() className = '';
  @Input() label = '';

  @Output() click: EventEmitter = new EventEmitter();

  onClick() {
     this.click.emit();
  }
}

ご覧のとおり、リファクタリングしたボタンは非常に簡潔です。 2 つの入力と 1 つの出力しか含まれません。 入力は、親コンポーネントからデータを取ってユーザーにそれを表示するために使用します(ボタンの外観はクラスと表示ボタンのラベルで変更します)。 出力は、ユーザーがボタンをクリックするたびに発行されるカスタムイベントに使用されます。

このコンポーネントは小さく、目的集中型、独立型、テスト可能性、および再利用可能性の特性を備えています。 コンポーネント自体の動作に無関係なロジックは含まれていません。 アプリケーションの残りの内部動作についてまったく認識しておらず、アプリケーションのあらゆる部分または他のアプリケーションにも安全にインポートして使用することが可能です。

リファクタリングした登録フォームコンポーネント(コンテナ)

@Component({
  selector: 'app-registration-form',
  template: `<form [formGroup]="userService.form">
  <input type="text" [formControl]="userService.form.get('username')" placeholder="Nickname">
  <input type="password" [formControl]="userService.form.get('password')" placeholder="Password">
  <input type="password" [formControl]="userService.form.get('passwordConfirm')" placeholder="Confirm password">
  <app-button className="button accent" label="Register" (click)="registerUser()"></app-button>
</form>
`
})
export class RegistrationFormComponent {
  constructor(public userService: UserService) {}

  registerUser() {
    const userFormData = this.userService.form.value;
    // some validation of user data
    // ...
    this.userService.registerUser(userFormData);
  }
}

この登録フォームは、ボタンを使用し、ボタンクリックに反応して registerUser メソッドを呼び出すようになりました。 このメソッドのロジックはこのフォームに密に関連しているため、それを配置する最適な場所と言えます。

これは非常に単純な例であり、コンポーネントツリーにあるレベルは 2 つだけです。 このパターンには、コンポーネントツリーのレベルが多い場合に落とし穴があります。

より高度な例

これは実際の例ではありませんが、このパターンに潜在する問題を理解する上で役立つと思います。

以下のようなコンポーネントツリーがあるとします(上から下)。

user-orders - トップレベルのコンポーネント。 サービスレイヤーに話しかけ、ユーザーとその注文に関するデータを受け取り、それを後続のツリーに渡して注文のリストをレンダリングするコンテナコンポーネントです。

user-orders-summary - 中間レベルのコンポーネント ユーザーの注文リストの上に合計注文数を表示するバーをレンダリングするプレゼンテーションコンポーネントです。

cashback - 最下レベル(リーフ/葉)コンポーネント。 ユーザーの合計キャッシュバック額を表示し、銀行口座に引き落とすためのボタンを持つプレゼンテーションコンポーネントです。

トップレベルのコンテナコンポーネント

トップレベルの user-orders コンテナコンポーネントを見てみましょう。

@Component({
  selector: 'user-orders',
  templateUrl: './user-orders.component.html'
})
export class UserOrdersComponent implements OnInit {
  user$: Observable<User>;
  orders$: Observable<Order[]>;

  constructor(
    private ordersService: OrdersService,
    private userService: UserService
  ) {}

  ngOnInit() {
      this.user$ = this.userService.user$;
      this.orders$ = this.ordersService.getUserOrders();
  }

onRequestCashbackWithdrawal() {
    this.ordersService.requestCashbackWithdrawal()
      .subscribe(() => /* notification to user that cashback withdrawal has been requested */);
    }
}
<div class="orders-container">
    <user-orders-summary
        [orders]="orders$ | async"
        [cashbackBalanace]="(user$  | async).cashBackBalance"
        (requestCashbackWithdrawal)="onRequestCashbackWithdrawal($event)"
    >
    </user-orders-summary>

    <div class="orders-list">
      <div class="order" *ngFor="let order of (orders$ | async)"></div>
    </div>
</div>

ご覧のとおり、user-orders コンポーネントは、user$orders$ の 2 つの Obeservable を定義し、非同期パイプをテンプレートに使用してそれらを購読しています。 このコンポーネントはデータをプレゼンテーションコンポーネントの user-orders-summary に渡し、注文リストをレンダリングします。 また、user-orders-summary から発行される requestCashbackWithdrawal カスタムイベントに反応してサービスレイヤーに話しかけます。

中間レベルのプレゼンテーションコンポーネント

@Component({
  selector: 'user-orders-summary',
  template: `
    <div class="total-orders">Total orders: {{orders?.length}}</div>
    <cashback [balance]="cashbackBalanace" (requestCashbackWithdrawal)="onRequestCashbackWithdrawal($event)"></cashback>
    `, 
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserOrdersSummaryComponent {
    @Input() orders: Order[];
    @Input() cashbackBalanace: string;

    @Output() requestCashbackWithdrawal = new EventEmitter();

    onRequestCashbackWithdrawal() {
        this.requestCashbackWithdrawal.emit();
    }
}

このコンポーネントは、上記でリファクタリングしたボタンコンポーネントに非常に似ています。 その入力から受け取ったデータをレンダリングし、何らかのユーザーアクションでカスタムイベントを発行します。 サービスの呼び出しは行わず、ビジネスロジックも含まれません。 そのため、これは別のプレゼンテーション cashback コンポーネントを使用する純粋なプレゼンテーションコンポーネントです。

最下レベルのプレゼンテーションコンポーネント

@Component({
    selector: 'cashback',
    template: `
<div class="cashback">
  <span class="balance">Your cashback balance: {{balance}}</span>
  <button class="button button-primary" (click)="onRequestCashbackWithdrawal()">Withdraw to Bank Account</button>
</div>
`,
    styleUrls: ['./cashback.component.css']
})
export class CashackComponent {
    @Input() balance: string;

    @Output() requestCashbackWithdrawal = new EventEmitter();

    onRequestCashbackWithdrawal() {
        this.requestCashbackWithdrawal.emit();
    }
}

これは、入力からデータを受け取って出力でイベントをスローするだけの、もう 1 つのプレゼンテーションコンポーネントです。 非常に単純で再利用可能ですが、コンポーネントツリーに問題があります

user-orders-summary コンポーネントと cashback コンポーネントには、類似する入力(cashbackBalanacebalance)と同じ出力(requestCashbackWithdrawal)があることに気づいたことでしょう。 これは、コンテナコンポーネントが一番深いプレゼンテーションコンポーネントから遠すぎるためです。 この設計ではツリーのレベル数が増えるほど、問題が悪化してしまいます。 この問題について詳しく見てみましょう。

問題 1 - 中間レベルのプレゼンテーションコンポーネントにある無関係なプロパティ

user-orders-summarycashbackBalanace の入力を受け取って、さらにツリーの下の方に渡すだけであり、それ自体がその入力を使用することはありません。 このような状況に直面した場合、コンポーネントツリーの設計に欠陥がある可能性があります。 実際の状況で使用するコンポーネントには多数の入力と出力があるため、この設計では多数の「プロキシ」入力が含まれてしまい、中間レベルのコンポーネントの再利用可能性が薄れてしまいます(子コンポーネントに密に連携させる必要があるため)。また、繰り返し可能なコードが多数含まれてしまいます。

問題 2 - 下方レベルからトップレベルのコンポーネントに湧き上がるカスタムイベント

この問題は、前の問題に非常に似ていますが、コンポーネントの出力に関連しています。 ご覧のとおり、requestCashbackWithdrawal カスタムイベントは、cashbackuser-orders-summary コンポーネントで繰り返されています。 これもやはり、コンテナコンポーネントが一番深いプレゼンテーションコンポーネントから遠すぎるために起きていることです。 また、中間コンポーネント自体を再利用できないものにしてしまっています。

これらの問題に対する潜在的な解決策は少なくとも 2 つあります。

1 - ngTemplateOutlet を使用して、中間レベルのコンポーネントをよりコンテンツに依存しないコンポーネントにし、より深いコンポーネントをコンテナコンポーネントに直接公開する。 この解決策には専用の記事にするだけの価値があるため、今回はこれについて説明しないでおきます。

2 - コンポーネントツリーを再設計する。

コンポーネントツリーをリファクタリングする

作成したコードをリファクタリングして、無関係なプロパティと中間レベルのコンポーネントで湧いているイベントの問題を解決できるか見てみましょう。

リファクタリングしたトップレベルのコンポーネント

@Component({
  selector: 'user-orders',
  templateUrl: './user-orders.component.html'
})
export class UserOrdersComponent implements OnInit {
  orders$: Observable<Order[]>;

  constructor(
    private ordersService: OrdersService,
  ) {}

  ngOnInit() {
      this.orders$ = this.ordersService.getUserOrders();
  }
}
<div class="orders-container">
    <user-orders-summary [orders]="orders$ | async"></user-orders-summary>

    <div class="orders-list">
      <div class="order" *ngFor="let order of (orders$ | async)"></div>
    </div>
</div>

トップレベルのコンテナコンポーネントから user$ Observable と onRequestCashbackWithdrawal() メソッドを削除しました。 非常に単純化され、user-orders-summary コンポーネント自体をレンダリングするために必要なデータのみを渡し、その子コンポーネントの cashback には渡さないようになっています。

リファクタリングした中間レベルのコンポーネント

@Component({
  selector: 'user-orders-summary',
  template: `
    <div class="total-orders">Total orders: {{orders?.length}}</div>
    <cashback></cashback>
    `, 
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserOrdersSummaryComponent {
    @Input() orders: Order[];
}

これもかなり単純化されています。 入力は 1 つだけで、合計注文数をレンダリングするようになっています。

リファクタリングした最下レベルのコンポーネント

@Component({
    selector: 'cashback',
    template: `
<div class="cashback">
  <span class="balance">Your cashback balance: {{ (user$ | async).cashbackBalance }}</span>
  <button class="button button-primary" (click)="onRequestCashbackWithdrawal()">Withdraw to Bank Account</button>
</div>
`,
    styleUrls: ['./cashback.component.css']
})
export class CashackComponent implements OnInit {
  user$: Observable<User>;

  constructor(
     private ordersService: OrdersService,
     private userService: UserService
  ) {}

  ngOnInit() {
    this.user$ = this.userService.user$;
  }

  onRequestCashbackWithdrawal() {
    this.ordersService.requestCashbackWithdrawal()
      .subscribe(() => /* notification to user that cashback withdrawal has been requested */);
    }
  }
}

すごいです。 ご覧のとおり、プレゼンテーションコンポーネントではなくなっています。 トップレベルのコンポーネントに非常に似ており、コンポーネントツリーの下にあるコンテナコンポーネントになりました。 このようにリファクタリングすることで、コンポーネントツリー全体の設計と API、およびコンポーネントツリーの上位にある 2 つのコンポーネントのロジックを単純化できました。

新しい cashback コンポーネントの再利用可能性についてはどうでしょうか? まだ、再利用可能なままです。それには、コンポーネント自体に関連するロジックのみが含まれているため、以前として独立性を維持しています。

新しいコンポーネントツリーの設計は、管理性、合理性、および細分化がさらに改善されているようです。 湧き上がってくるイベントが無くなり、コンポーネントツリー全体で繰り返し可能な入力もありません。総合的な設計もはるかに単純になっています。 これは、コンポーネントツリーの下にコンテナコンポーネントを追加することで達成しました。 この手法は、コンポーネントツリーの設計を単純化するために使用できますが、再利用可能性を大幅に失うことなく、ツリー内のどのコンポーネントをコンテナにするのが適しており、どれが純粋なプレゼンテーションコンポーネントであるべきかを十分に理解しておくことが必要です。 これはバランスと設計の選択に関する課題であり、アプリのアーキテクチャを作成するときには必ず発生するものです。

「コンテナ・プレゼンテーション」パターンは非常に誤解しやすいもので、コンテナコンポーネントは必ずトップレベルコンポーネントであるべきだと考えがちです(直感的に、コンテナはローカルコンポーネントツリー内のその他すべてのコンポーネントを格納するものです)。 しかし、そうではありません。コンテナコンポーネントはコンポーネントツリーのあらゆるレベルに配置することができます。上記で見たように、リーフレベルにも配置可能です。 私はコンテナコンポーネントをスマート(賢い)コンポーネントと呼んでいます。私にとって、これらのコンポーネントがビジネスロジックを含み、コンポーネントツリーのあらゆる場所に配置できるということが非常に明白であるためです。

後書き

ここまでで、「コンテナ・プレゼンテーション」パターンの概要とその実装における潜在的な問題について理解できたことと思います。

できるだけ単純に維持するよう努めましたが、このパターンに関しては非常に多くの情報が存在します。

ご質問やメッセージがございましたら、お気軽にコメント欄に投稿してください。

次回の記事では、Angular における changeDetectionStrategy についてお話しします(この記事に非常に関連する内容です)。

それではまた!

0
0 764
記事 Toshihiko Minamoto · 11月 29, 2022 11m read

中・上級トピックに進む前に、より一般的なポイントについてまとめておきたいと思います。 これはもちろん主観的な内容であるため、他の意見やさらに良い根拠をお持ちであれば、それについて喜んでお聞きします。

このリストは包括的ではありません。一部のトピックは今後の記事で対応するつもりなので、意図的にそうしています。

ヒント 1. 公式スタイルガイドに従う

Angular は、アプリケーションに使用可能なアーキテクチャを制限するという点で非常に厳格ですが、それでも独自の方法で行えることはたくさんあります。 開発者の想像力は無限ではありますが、そのために、あなたと、またはあなたを引き継いでプロジェクトに携わる人の作業が困難になってしまうことがあります。

Angular チームは、Angular アーキテクチャ自体とそのライブラリをうまく管理しているため、安定した対応可能なコードベースを作成する方法をよく理解しています。

したがって、公式スタイルガイドに従い、そのとおりに動作しない場合にのみ他の方法を取るようにすることをお勧めします。 こうすることで、新しいプロジェクトに参加する際や、他の人がプロジェクトに参加する際に、事がより簡単に進められるようになります。

コードとアプリアーキテクチャが対応可能で安定しており、理解しやすい方が、賢明でありながらも暗号的なソリューションを作るよりも重要です。誰も追いつけなくなってしまいます(開発者自身が後に追いつけなくなる可能性もあります)。

Angular 公式スタイルガイド: https://angular.io/guide/styleguide

ヒント 2. Angular の Ninja 本を購入する

宣伝と思われるかもしれませんが、つまりこういうことです。Angular のすべての主要概念がカバーされた非常に良い本で、価格はあなたが決められます。 また、価格のどれくらいが作者に支払われ、どれくらいが慈善活動に募金されるかを決めることもできます。

この本の作者の 1 人は Angular チームのメンバーであるため、公式ドキュメントの次に最も信頼できる Angular の情報源であることは間違いありません。 本の章構成は、本のページで確認できるようになっているため、サンプルページを読んでから購入する価値があるかを決めることができます。

さらに、この本は Angular の新しいバージョンのリリースで更新されており、本のすべての更新内容を無料で得られます。

Ninja Squad ブログ自体も Angular に関する非常に優れた情報源で、新しいバージョンに関する記事や最新情報、ベストプラクティス、実験的機能などが掲載されています。

Angular の Ninja 本: https://books.ninja-squad.com/angular

ヒント 3. 公式ドキュメントを読む

アプリのコードを書き始める前、特に、以前に使用したことのないバージョンの Angular でプロジェクトに取り組む場合には、公式ドキュメントとガイドに目を通すことをお勧めします。 廃止機能は常に増えており、インターネット上のチュートリアルとガイドが古くなっている可能性があります。そのため、新しいベストプラクティスや機能を使用する代わりに、技術的負債が増え続けてしまうことになりかねません。

ガイドがどのバージョンを対象にして書かれているのかを確認するのも良い習慣です。 使用しているのが 2 つ以上前のバージョンであれば、それが書かれた当時から変更されたことがないかを確認することをお勧めします。

公式ドキュメント: https://angular.io

ヒント 4. Angular Material を使用しなくても、Angular CDK の使用を検討する

これについては今後の記事に取り上げるのが良いと思いますが、私は、多くの Angular 開発者は Angular CDK の存在すらも知らないことを知っています。

Angular CDK は、より優れたアプリケーションの開発を支援できる便利なディレクティブと基底クラスが集められたライブラリです。 たとえば、FocusTrap、Drag & Drop、VirtualScroll などが含まれており、コンポーネントに簡単に追加することができます。

Angular CDK: https://material.angular.io/cdk/categories

ヒント 5. package.json 内の依存関係を修正する

特に Angular に関連することではありませんし、どんなプロジェクトでも重要なことかもしれません。 npm install --save <something> を実行すると、パッケージバージョンの先頭に ^ または ~ 付きで package.json に追加されます。 つまり、プロジェクトはその依存関係と同じメジャーバージョンのあらゆるマイナー/パッチバージョンを使用できるということになります。 この場合、将来的にほぼ 100% の確率で問題となります。 私はさまざまなプロジェクトにおいて何度もこの問題に直面しました。 時間が経過し、ある依存関係の新しいマイナーバージョンが作成されれば、アプリはビルドできなくなります。 この依存関係の作成者がそれをマイナー(またはパッチ)バージョンで更新したために、ビルドと競合するようになってしまうこともあるでしょう。 また、依存関係の新しいマイナーバージョンで新しいバグが見つかることもあります。自分のコードでは問題がなくても(新しいバージョンがリリースされる前に依存関係をインストールしたため)、リポジトリから自分のプロジェクトをインストールしてビルドしようとしている人たちが、自分が気付いていないバグに直面することもあります(また、自分のマシンでそのバグを再現することもできません)。

package-lock.json は、この問題を解決し、複数の開発者がプロジェクト全体で同じ依存関係セットを使用できるようにするために存在します。 リポジトリにコミットすることが理想的ですが、このファイルを .gitignore に追加している開発者がたくさんいます。 それでは意味がなく、結局、上記と同じ問題が起きてしまいます。 そのため、依存関係の解決を盲目的に信頼して特定のバージョンに固定するのではなく、定期的に脆弱性をスキャンして(npm audit を使用)、手動で更新してテストすることをお勧めします。

ヒント 6. できるだけ早期にアプリを新しい Angular バージョンにアップグレードするようにする

Angular は絶えず進化しており、約半年ごとに新しいバージョンがリリースされています。 アプリを最新状態に維持することで、ビルドの高速化やその他の最適化を含むすべての新機能を使用することができます。 この場合には、Angular の次期メジャーバージョンへのアップグレードは大した問題ではないでしょう。 ただし、Angular には、Angular の中間バージョンをスキップしてアプリをアップグレードするためのスケマティックがないため、5 バージョン前の Angular を使用しており、アプリケーションの規模が中~大である場合には、最新バージョンへの更新プロセスが困難になる可能性があります。 各バージョンでアプリケーションが動作することを確認しながら、すべての中間バージョンを 1 つずつアップグレードすることが必要になるでしょう。 Angular CLI のスケマティックを使わずに直接アップデートすることは可能ですが、コツが必要となるため、常にアプリを最新状態に維持しておくことをお勧めします。

ヒント 7. 依存関係リストをできるだけ短くする

新機能が必要となるたびに新しい依存関係をアプリに採り入れるという習慣に陥りやすいものですが、 依存関係ごとに、アプリの複雑性が増し、技術的負債が突然雪崩れてしまうリスクが高まります。

アプリに新しい依存関係を追加すると決めた場合は、以下のことを検討してください。

  • どれほど十分に依存関係がサポートされているか?
  • どれくらいの人が使用しているか?
  • 開発チームはどれくらいの規模か?
  • どれくらい迅速に GitHub 課題をクローズしているか?
  • どれくらい十分に依存関係のドキュメントが書かれているか?
  • Angular の新しいメジャーバージョンがリリースされてからどれくらい迅速に更新されているか?
  • この依存関係によるアプリケーションのパフォーマンスとバンドルサイズへのインパクトはどれくらいか?
  • 将来的に放置または廃止された場合に、この依存関係を入れ替えるのがどれくらい困難になるか?

この質問のいくつかに否定的な回答がある場合、その依存関係を排除するか、少なくとももっと成熟して十分にサポートされているものに交換することを検討してください。

ヒント 8. 「暫定的な」型として <any> を使用しない

繰り返しますが、適切な型を書くには時間がかかるものであり、このスプリントでタスクを完了させる必要があるため、ビジネスロジックを書く際に簡単に any を使いがちです。 もちろん、型は後で追加できますが、 技術的負債がすぐに積もる危険があります。

アプリと型のアーキテクチャは、ビジネスロジックを書く前に定義しておく必要があります。 どのようなオブジェクトがあり、どこで使用されるかを明確に理解しておくかなければなりません。 コードの前に仕様書、ですよね? (Tom Demarco は、この考えが主流になる前にこれについての本を書いています: https://www.amazon.com/Deadline-Novel-About-Project-Management/dp/0932633390

事前に定義された型を使わずにコードを書いている場合、非常に似ているようで異なるオブジェクトを使用する、質の悪いアプリアーキテクチャと関数が出来上がってしまう可能性があります。 そのため、関数ごとに異なる型を作成する(余計に悪くなってしまいます)か、仕様書、型、およびリファクタリングを書くことに時間を費やす必要があるでしょう。後者は、前もってやっていれば時間の無駄になりません。

ヒント 9. ビルドプロセスを理解するのに時間をかける

Angular は、プロジェクトの構築に関する開発者の作業をうまく容易にしていますが、 デフォルトのオプションは、すべての場合に必ずしも最適ではありません。

Angular のビルドプロセスの仕組み、開発ビルドと本番ビルドの違い、Angular で提供されているビルドのオプション(最適化、ソースマップ、バンドル化など)を理解する時間を取りましょう。

ヒント 10. バンドル内のものを調べる

すべてのライブラリがツリーシェイキングを提供しているわけでも、必ずしもすぐにインポートするわけでもありません。そのため、冗長するものがアプリにバンドルされる可能性が必ずあります。

したがって、たまにバンドルの中身を調べる習慣をもつことをお勧めします。

webpack-bundle-analyzer を使ったプロセスをうまく説明した記事がいくつかあるため、ここでは説明しませんが、その中の 1 つのリンクをご覧ください: https://www.digitalocean.com/community/tutorials/angular-angular-webpack-bundle-analyzer

このトピックについては、連載の後の方でより詳しく説明します。

ヒント 11. モジュールにサービスをインポートする代わりに、サービスの providedIn プロパティを使用する

インターネットに公開されている Angular コードサンプルの多くは、モジュールへのサービスのインポートを使用しています。 ただし、Angular 6 または 7 以降では、これはサービスを宣言する方法として推奨されていません。 ツリーシェイキングを有効にし、アプリケーションのバンドル化を改善するには、@Injectable デコレータのプロパティ providedIn を使用する必要があります。 Angular は、どのバンドルにサービスを含める必要があるのか、いつ初期化すべきなのか、サービスのインスタンスをいくつ作成する必要があるのかを理解できるほどスマートなフレームワークです。

providedIn が受け入れる値には 3 つあります。 ほとんどの場合は root で十分ですが、そのほかに 2 つあります。

  • root: サービスアプリケーションの範囲でシングルトンになります。
  • any: すべての Eager ロードされたモジュールにサービスのインスタンスが 1 つ作成され、遅延モジュールごとに別のインスタンスが作成されます。
  • platform: 同じページで実行するすべてのアプリケーションに対し、サービスはシングルトンになります。

ヒント 12. 主要なパフォーマンスルールを忘れないこと

  • 再描画操作と JavaScript 実行を減らすには、コレクションに trackBy を使用する
  • 使用できるすべての場所に onPush 変更検出ストラテジーを使用する(これについては、専用の記事で説明します)
  • 高い処理能力を必要とする計算は、ngZone の外で実行する
  • イベントにスロットル・デバウンスパターンを使用して、不要なサーバー呼び出しとイベントの大量送信を防止する
  • ビッグデータセットの表示には仮想スクロールを使用する
  • テンプレートにデータ変換用の純粋なパイプを使用する
  • AoT コンパイルを使用する
  • アプリケーションの起動には必要ないアプリの部分を遅延読み込みする
  • テンプレートでは計算と条件付き関数呼び出しを使わない(関数呼び出しはイベントのみに使用します)

お読みいただきありがとうございました! いくつかのヒントがお役に立ちますように。 コメントやご意見がございましたら、コメント欄でお知らせください。喜んでお伺いします 😃

それではまた!

0
0 141
記事 Toshihiko Minamoto · 11月 17, 2022 10m read

こんにちは! Sergei Sakisian と申します。InterSystems で 7 年以上、Angular フロントエンドを作成しています。 Angular は非常に人気のあるフレームワークであるため、開発者、お客様、そしてパートナーの皆さんは、アプリケーションのスタックの 1 つとして Angular を選択することがよくあります。

概念、ハウツー、ベストプラクティス、高度なトピックなど、Angular のさまざまな側面を網羅する記事の連載を始めたいと思います。 この連載は、すでに Angular に精通しており、基本概念の説明がいらない方が対象となります。 連載記事のロードマップを作成しているところであるため、まずは、一番新しい Angular リリースの重要な機能をいくつか紹介することから始めることにします。

厳格な型指定のフォーム

これはおそらく、過去 2 年間で最も要望の多かった Angular 機能です。 Angular 14 では、Angular リアクティブフォームを使って、TypeScript のすべての厳格な型チェック機能を使用できるようになりました。

FormControl クラスはジェネリクスになったため、それが保持する値の型を取ることができます。

/* Angular 14 より前*/
const untypedControl = new FormControl(true);
untypedControl.setValue(100); // 値を設定、エラーなし

// 現在
const strictlyTypedControl = new FormControl<boolean>(true);
strictlyTypedControl.setValue(100); // ここで型チェックエラーメッセージが表示されます

// Angular 14
const strictlyTypedControl = new FormControl(true);
strictlyTypedControl.setValue(100); // ここで型チェックエラーメッセージが表示されます

ご覧のとおり、最初の最後の例はほぼ同じですが、結果が異なります。 これは、Angular 14 では、新しい FormControl クラスが、開発者が指定した初期値から型を推論しているためです。 したがって、true が指定された場合、Angular はこの FormControl の型を boolean | null に設定します。 .reset() メソッドには、値が指定されていない場合に値を null にする Nullable 値が必要です。

以前の型なしの FormControl クラスは、UntypedFormControl に変換されています(UntypedFormGroupUntypedFormArray、および UntypedFormBuilder についても同様)が、実質的に FormControl<any> のエイリアスです。 以前のバージョンの Angular からアップグレードしている場合、FormControl クラスのすべてのメンションは、Angular CLI によって UntypedFormControl クラスに置き換えられます。

Untyped* のクラスは、以下のような特定の目的に使用されます。

  1. アプリを、以前のバージョンから移行される前とまったく同じように動作させる(新しい FormControl は、初期値から型を推論することに注意してください)。
  2. すべての FormControl<any> を意図的に使用する。 そのため、すべての UntypedFormControl を手動で FormControl<any> に変更する必要があります。
  3. 開発者にもっと自由度を与える(これについては、後の方で説明します)。

初期値が null である場合、FormControl の型を明示的に指定する必要があることに注意してください。 また、TypeScript には、初期値が false の場合に同じことを行う必要のあるバグが存在します。

フォームの Group については、インターフェースを定義することも可能です。このインターフェースを FormGroup の型として渡すだけです。 この場合、TypeScript は FormGroup 内のすべての型を推論します。

interface LoginForm {
    email: FormControl<string>;
    password?: FormControl<string>;
}

const login = new FormGroup<LoginForm>({
    email: new FormControl('', {nonNullable: true}),
    password: new FormControl('', {nonNullable: true}),
});

手動で FormGroup を作成した上記の例のように、FormBuilder の .group() メソッドに、事前に定義されたインターフェースを受け入れられるジェネリクス属性が追加されました。

interface LoginForm {
    email: FormControl<string>;
    password?: FormControl<string>;
}

const fb = new FormBuilder();
const login = fb.group<LoginForm>({
    email: '',
    password: '',
});

このインターフェースにはプリミティブな非 nullable 型しかないため、新しい nonNullable FormBuilder プロパティ(NonNullableFormBuilder クラスインスタンスを含み、直接作成することも可能)を使って以下のように単純化できます。

const fb = new FormBuilder();
const login = fb.nonNullable.group({
    email: '',
    password: '',
});

❗ 非 nullable 型の FormBuilder を使用する場合、または FormControl に非 nullable 型のオプションを設定する場合、.reset() メソッドを呼び出す際に、リセット値として初期の FormControl 値が使用されることに注意してください。

また、this.form.value のすべてのプロパティがオプションとしてマークされることに注意することも非常に重要です。 以下に例を示します。

const fb = new FormBuilder();
const login = fb.nonNullable.group({
    email: '',
    password: '',
});

// login.value
// {
//   email?: string;
//   password?: string;
// }

これは、FormGroup 内のいずれかの FormControl を無効にする際に、この FormControl の値が form.value から削除されるために発生します。

const fb = new FormBuilder();
const login = fb.nonNullable.group({
    email: '',
    password: '',
});

login.get('email').disable();
console.log(login.value);

// {
//   password: ''
// }

フォームオブジェクト全体を取得するには、.getRawValue() メソッドを使用する必要があります。

const fb = new FormBuilder();
const login = fb.nonNullable.group({
    email: '',
    password: '',
});

login.get('email').disable();
console.log(login.getRawValue());

// {
//   email: '',
//   password: ''
// }

厳格に型付けされたフォームのメリット:

  1. FormControl / FormGroup の値を返すすべてのプロパティとメソッドが厳格に型付けされるようになった。 例: valuegetRawValue()valueChanges
  2. FormControl 値を変更するすべてのメソッドが型安全になった。setValue()patchValue()updateValue()
  3. FormControl が厳格に型付けされた。 このことは、FormGroup の .get() メソッドにも適用されます。 これにより、コンパイル時に存在しない FormControl へのアクセスも防止されます。

新しい FormRecord クラス

新しい FormGroup クラスの欠点は、その動的な性質が失われたことです。 一度定義されると、オンザフライで FormControl を追加または削除することはできません。

この問題を解決するために、Angular は新たに FormRecord クラスを追加しました。 FormRecord は実質的に FormGroup と同じですが、動的であり、そのすべての FormControl に同じ型が使用されます。

folders: new FormRecord({
  home: new FormControl(true, { nonNullable: true }),
  music: new FormControl(false, { nonNullable: true })
});

// グループに新しい FormContol を追加する 
this.foldersForm.get('folders').addControl('videos', new FormControl(false, { nonNullable: true }));

// コントロールの型が異なるため、これにより、コンパイルエラーが発生する
this.foldersForm.get('folders').addControl('books', new FormControl('Some string', { nonNullable: true }));

ご覧のとおり、これには別の制限があります。すべての FormControl は同じ型でなければなりません。 動的と異種の両方を兼ね備えた FormGroup がどうしても必要な場合は、UntypedFormGroup クラスを使用してフォームを定義することをお勧めします。

モジュールレス(スタンドアロン)コンポーネント

これは未だ実験的とされている機能ではありますが、興味深い機能です。 コンポーネント、ディレクティブ、およびパイプをモジュールに含めることなく、これらを定義することができます。

この概念はまだ完全に練られてはいませんが、すでに ngModule を使用せずにアプリケーションをビルドすることができるようになっています。

スタンドアロンコンポーネントを定義するには、Component/Pipe/Directive デコレーターで新しい standalone プロパティを使用する必要があります。

@Component({
  selector: 'app-table',
  standalone: true,
  templateUrl: './table.component.html'
})
export class TableComponent {
}

この場合、このコンポーネントはどの ngModule にも宣言されませんが、 ngModule やその他のスタンドアロンコンポーネントにインポートすることは可能です。

各スタンドアロンコンポーネント/パイプ/ディレクティブには、その依存関係を直接デコレーターにインポートするメカニズムが備えられています。

@Component({
  standalone: true,
  selector: 'photo-gallery',
  // 既存のモジュールは直接スタンドアロンコンポーネントにインポートされる
  // CommonModuleは、*ngIf などの標準の Angular ディレクティブを使用するために直接インポートされる
  // 上記に宣言されるスタンドアロンコンポーネントも直接インポートされる
  imports: [CommonModule, MatButtonModule, TableComponent],
  template: `
    ...
    <button mat-button>Next Page</button>
    <app-table *ngIf="expression"></app-table>
  `,
})
export class PhotoGalleryComponent {
}

前述のとおり、スタンドアロンコンポーネントは、既存の ngModule にインポート可能です。 sharedModule 全体をインポートする必要がなく、本当に必要な物だけをインポートできます。 新しいスタンドアロンコンポーネントを使用し始めるのに適したストラテジーでもあります。

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule, HttpClientModule, TableComponent], // import our standalone TableComponent
  bootstrap: [AppComponent]
})
export class AppModule {}

スタンドアロンコンポーネントは、Angular CLI を使って以下を入力すると作成できます。

ng g component --standalone user

モジュールレスアプリケーションをブートストラップ

アプリケーションにあるすべての ngModule を排除する場合は、別の方法でアプリをブートストラップする必要があります。 Angular にはこのための新しい関数があり、それを main.ts ファイルで呼び出す必要があります。

bootstrapApplication(AppComponent);

この関数の 2 つ目のパラメーターを使って、アプリ全体で必要なプロバイダーを定義できます。 通常プロバイダーのほとんどはモジュール内に存在するため、Angular は(現時点では)それに新しい importProvidersFrom 抽出関数を使用する必要があります。

bootstrapApplication(AppComponent, { providers: [importProvidersFrom(HttpClientModule)] });

スタンドアロンコンポーネントの遅延読み込みルート:

Angular には、loadComponent という新しい遅延読み込みルート関数があります。これは、スタンドアロンコンポーネントを読み込むためだけに存在する関数です。

{ 
  path: 'home',
  loadComponent: () => import('./home/home.component').then(m => m.HomeComponent)
}

loadChildren は、ngModule を遅延読み込みできるようにするだけでなく、ルートファイルから直接、子ルートも読み込めるようになっています。

{ 
  path: 'home',
  loadChildren: () => import('./home/home.routes').then(c => c.HomeRoutes)
}

記事の執筆時点におけるいくつかの注意事項

  • スタンドアロンコンポーネント機能は、現在も実験的段階にあります。 将来的に、Webpack の代わりに Vite ビルダーに移行し、ツーリングの改善、ビルド時間の高速化、アプリアーキテクチャの強化、テスト方法の改善などを通じて、機能が大幅に改善されるでしょう。 現時点では、こういったものが多数欠けているため、全パッケージを受け取っていません。いずれにせよ、少なくともこの新しい Angular パラダイムを念頭に、アプリを開発し始めることは可能です。
  • IDE と Angular ツールはまだ、新しいスタンドアロンエンティティを静的に解析する準備を整えていません。 すべての依存関係を各スタンドアロンエンティティにインポートする必要があるため、何かを見逃した場合、コンパイラーもそれを見逃し、ランタイム時に失敗する可能性があります。 これは今後改善されていきますが、現時点ではインポートの際に開発者側の注意が必要です。
  • 現時点では Angular にグローバルインポート機能がないため(Vue などで行われるように)、各スタンドアロンエンティティで、依存関係を確実に 1 つずつインポートする必要があります。 この機能の主な目標は、私が思うところ、ボイラープレートを減らして物事を簡単に実行できるようにすることにあるため、今後のバージョンで解決されることを期待しています。

今日は、これで以上です。 それではまた!

0
0 1049
記事 Shintaro Kaminaka · 11月 27, 2020 5m read

背景

InterSystems IRIS 2019 では、新たに魅力的な機能が導入される予定です。 ぜひ知っておくべき魅力的な新機能の一つには、API 管理があります。

OpenAPI Initiative(https://www.openapis.org/)は、API を定義するための標準仕様(https://github.com/OAI/OpenAPI-Specification)をサポートする組織です。 OpenAPI 仕様(OAS)は、REST API 向けのプログラミング言語に依存しない標準的なインターフェースの記述を定義するもので、人間とコンピューターの両方が、ソースコードへのアクセス、追加ドキュメント、またはネットワークトラフィックの検査を必要とせずに、サービスの機能を検出して理解できるようにしています。 OpenAPI を使用して適切に定義されている場合、消費者は最小限の実装ロジックでリモートサービスを理解して対話できます。 低レベルのプログラミングに対するインターフェース記述と同様に、OpenAPI 仕様によってサービスを呼び出す際の当て推量が排除されます。

InterSystems は InterSystems IRIS で API 設計優先のアプローチをサポートしており、それによって先に仕様を設計してからサーバーサイドを生成できるようにしています。 API を先に設計する場合、通常は Swagger Editor やその他同様のツールを使用して仕様を作成し、必要に応じて JSON 形式で OAS 仕様を取得します。

API を設計して実装する準備ができたら、OAS 仕様を使用してサーバーサイドの API ロジックを作成できるようになります。 InterSystems IRIS 2019.1 では、新しいルーチンである ^%REST を使用し API を土台にして、ビジネスロジックを呼び出すコードを配置するクラスを自動的に生成できます。 このクラスのメソッドは命名規則に基づいて生成されますが、メソッドとクラスを仕様(operationId)で定義することもできます。

InterSystems IRIS REST コマンドラインインターフェースの使用例を以下に示します。

USER>do ^%REST
 
REST Command Line Interface (CLI) helps you CREATE or DELETE a REST application 
Enter an application name or (L)ist all REST applications (L): acmeapi
REST application not found: acmeapi
Do you want to create a new REST application? Y or N (Y):
 
File path or absolute URL of a swagger document.
If no document specified, then create an empty application.
OpenAPI 2.0 swagger: C:\myspec\acme.swagger.json
 
OpenAPI 2.0 swagger document: C:\myspec\notification.swagger.json
Confirm operation, Y or N (Y):
-----Creating REST application: acmeapi-----
CREATE acmeapi.spec
GENERATE acmeapi.disp
CREATE acmenapi.impl
REST application successfully created.
 
Create a web application for the REST application? Y or N (Y):
Specify web application name. Default is /csp/api/acme
Web application name: /csp/api/acme/v1
 
-----Deploying REST application: acmeapi-----
Application acmeapi deployed to /csp/api/acme/v1
 

現時点では、REST API を作成する際に API の土台の構築に使用できるのは OpenAPI 2.0 Swagger 仕様だけです。

ご覧のとおり、このルーチンは以下の 3 つのクラスを作成します。

  • .spec: このクラスは swagger 仕様(XData OpenAPI ブロック)のコンテナです。 このクラスは読み取り専用です。
  • .disp: CSP アプリケーションで使用できるディスパッチクラスです。 %CSP.REST を拡張し、XData UrlMap を定義するものです。 また、このクラスは読み取り専用であり、システムクラスとして扱われます(デフォルトでは Atelier で非表示になっています)。
  • .impl: 必要なすべての署名メソッドを定義するクラスです。 API を機能させるには、このクラスを完成させる必要があります。

すでに開発済みの API がある場合

InterSystems は、開発者がリモートで API 機能を探索できるサービス検索機能を InterSystems IRIS 2018.1 で導入しました。 また、Swagger を統合することで、既存の REST アプリケーションから Open API 仕様(OAS)を生成できるようにしています。 そのため、InterSystems IRIS で変更される API はすべて swagger 仕様を自動生成できます。

次のように管理 API を使用すると、システムで使用可能なすべての API を照会することができます。

HTTP GET http://:/api/mgmnt/ 

次の結果が返ってきます。


[
...,
    {
        "name": "/csp/petstore/v2",
        "dispatchClass": "petstore.disp",
        "namespace": "USER",
        "resource": "",
        "swaggerSpec": "/api/mgmnt/v1/USER/spec/csp/petstore/v2",
        "enabled": true
    }
]

また、API の Swagger 仕様は swaggerSpec プロパティによって示された URL に HTTP GET を発行することで取得できます。 元の swagger 仕様で定義されている API 操作には、アクションを実装する必要があるメソッドの名前を定義する新しいプロパティが追加されています。

例:

"x-ISC_ServiceMethod": "getPetById",

この api/mgmnt は検索だけではなく、次のように API の作成/照会/削除にも使用できるのが非常に魅力的です。

HTTP POST to /api/mgmnt/v2//
HTTP GET to /api/mgmnt/v2//
HTTP DELETE to /api/mgmnt/v2//

IRIS API Explorer

IRIS Explorer はこの API を利用し、IRIS API を非常にわかりやすく視覚化して管理するためのツールを提供する Angular 5 アプリケーションです。 以下、簡単にご紹介します。

まず、InterSystems IRIS インスタンスにログインする必要があります(デフォルトでは、52773 番ポートでローカルインスタンスを探します)。

ログイン後、アプリがクエリを実行して既存のすべての API を取得します。

ここでは既存の API を削除することも、新しい API を作成することもできます。 新しいアプリケーションを作成するには、ネームスペース / アプリケーション名 / Swagger 仕様を含む .json ファイルを指定する必要があります。

API を作成したら、仕様を表示できるようになります。 私はさらにこれを見やすくするため、Swager-UI(https://github.com/swagger-api/swagger-ui)を埋め込みました。

もちろん、JSON 形式の OSA 仕様も取得できます。

すべてのコードはオープンであり、都合に合わせて使用または変更することができます。 このアプリは Open Exchange で入手できます。

https://openexchange.intersystems.com/package/iris-explorer

GitHub でも公開しています。

https://github.com/drechema/iris-explorer

お役に立てば幸いです。

0
0 314