EditorとFileMakerの連携を楽にする（WebViewer開発）

WebViewerで表示させるコードはごく簡単なものでない限りVSCodeなど専用のコードエディタを使って開発しますが、ブラウザでは問題なく表示されていても、いざFileMakerに組み込むと何故か表示されないとか、UIがおかしいとかってよくあります。ここで詰まると原因究明に割と時間がかかります。win/macでも挙動が違いますし。

なので、エディタ上で行った修正をコピペせずにすぐにFileMaker上で確認したいですよね。細かい単位で。

 ここでは、FileMakerファイルがクラウド上（公開されたネットワーク上）にある事が前提となりますが、エディタの完成コードをシームレスにFileMakerに反映させる手法について書きます。

### 動機
JSON関数の充実や、WebViewerとFileMakerレコードとのデータのやり取りの敷居が低くなってきた事などで、WebViewerを利用してUIを作成する事がかなり増えてきました。見た目の美しさ・柔軟なUI設計など別次元ですので。
しかしそれに伴い、WebViewer側のコードもモリモリと複雑化し、そうなるとちょっとした修正でも色んな面で注意を払う必要が出てきます。

今まではビルドしてできた一枚のindex.htmlをコピーしてFileMakerのレコードに保存していました。おそらく多くの方がこうしていると思います。そしてコピペという作業自体が億劫なので、ある程度開発した段階で、そろそろFileMakerで確認するかなとレコードに貼り付ける。つまり細かいスパンで実環境での動作を確認する事なくほぼほぼ完成した段階でようやく実環境で試してみる事になります。まあ小規模なものならこれでもいいですが、フレームワークを利用してWEBアプリ並みの機能を実装させていると、度々動かない（何も表示されない）場面に出くわすはずです。そして原因を探すのに時間を取られる。開発の初期段階からもっと細かいスパンで確認を繰り返しておけば原因の究明も早いのに。

### 結論
先に結論を言いますと、ビルドしたコードをdataAPI経由でレコードに保存するだけ。

単純ではありますが、これだけです。今までもそうしたいなと漠然と思っていましたが、まあいつかやろうと後回しにしていたのが正直なところです。

node.jsで実装して（ここではsend.jsというファイルに）package.jsonのスクリプトに登録

``` package.json
"scripts":{
"build": "tsc -b && vite build",
"send": "node send.js",
"sendfm": "npm run build && npm run send",
}
```    

開発中は細かいスパンで以下を実行するだけ

```
npm run sendfm

```

これ一発でFileMaker側の所定のレコードにコードが反映されるので、コピペどころかGit commitと同じ感覚で実環境での動作を確認しながら開発できます。エディタとFileMakerがシームレスになり、超絶楽になりました。ちょっと前のコードに戻したい時はGitで戻してまたスクリプトを叩くだけなので、とても気が楽です。
なんでもっと早くやらなかった？となります。

※一般的なビルドツールは複数のファイルに分割しますが、index.htmlファイル一枚にまとめてくれるプラグインがそれぞれに存在するはずです。

※生のJavaScriptで書いてビルドの必要がなければ、タスクランナーなどで一枚のhtmlにまとめて下さい。まあ複数ファイルでも問題ないのですが、FileMaker側でその分フィールドを増やして連結するの面倒なので、一枚にまとめた方がシームレス感が増します。

参考までに上で作ったsend.jsの内容は以下ですが、実装しているのはtokenの取得、廃棄、レコードの更新関数だけですので、設定関係以外はAIに質問して好きなプログラムコードで書いてもらえばすぐです。

```send.js

import fs from 'fs'
import fetch from 'node-fetch'
import dotenv from 'dotenv'

dotenv.config()

const filePath = './dist/index.html' // FileMakerに保存するファイルのパス
const host = process.env.HOST
const database = process.env.DATABASE
const layout = process.env.LAYOUTNAME
const recordId = process.env.RECORD_ID // 更新するレコードのID
const baseUrl = `https://${host}/fmi/data/vLatest/databases/${database}`
const username = process.env.USERNAME
const password = process.env.PASSWORD
const auth = Buffer.from(`${username}:${password}`).toString('base64')
let token = null

// トークンを取得する関数
async function getToken() {
  const url = `${baseUrl}/sessions`
  try {
    const response = await fetch(url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Basic ${auth}`,
      },
      body: JSON.stringify({}),
    })

    const data = await response.json()
    return data
  } catch (error) {
    console.error('Error:', error.message)
  }
}

// トークンを破棄する関数
async function deleteToken() {
  try {
    const url = `${baseUrl}/sessions/${token}`
    const response = await fetch(url, {
      method: 'DELETE',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Basic ${auth}`,
      },
    })
    const data = await response.json()
    console.log('Token deleted :', JSON.stringify(data))
  } catch (error) {
    console.error('Error:', error.message)
  }
}

// レコードを更新する関数
async function updateRecord() {
  const htmlContent = fs.readFileSync(filePath, 'utf8') // index.htmlの内容を読み込む
  const url = `${baseUrl}/layouts/${layout}/records/${recordId}`
  try {
    const response = await fetch(url, {
      method: 'PATCH',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${token}`,
      },
      body: JSON.stringify({ fieldData: { html: htmlContent } }), // htmlフィールドを更新
    })

    const data = await response.json()
    console.log('Record updated:', data)
    return data
  } catch (error) {
    console.error('Error:', error.message)
  }
}

// メイン関数
async function main() {
  const tokenData = await getToken()
    if (tokenData && tokenData.response && tokenData.response.token) {
      token = tokenData.response.token
    } else {
      console.log('Failed to retrieve token.')
      return
    }
    await updateRecord()
    await deleteToken()
    console.log('send.js executed successfully.')
  
}

main()

```

環境変数
```.env
USERNAME=<your-userName>
PASSWORD=<your-userPassword>
HOST=<your-hostName>
DATABASE=<your-databaseName>
RECORD_ID=<recordId>
LAYOUTNAME=<layoutname>

```

## 環境・FileMaker側の設定
大まかですが、FileMaker側のWebViewerの表示設定などを以下に

### 開発環境
 - vscode, coursorなどのコードエディタ
 - node.jsをインストール済み
 - dataAPIの利用が可能になっている
 - FileMakerのバージョンは19以上

 ### WebViewerへ渡すコードの管理
 - コード保管用として専用のテーブル（例えばcodeというテーブル）を用意
 - 作成するフィールド
     - html (コードを格納)
     - callName　(対象レコード呼び出し用の名前を適当に)
     - objectName　(WebViewerにつけた名前)
     - recordId (計算値でGet(レコードID）、このレコードIDを.envのRECORD_IDに登録する
 - このテーブルを表示するレイアウトを用意し、上記3つのフィールドをレイアウト上に配置（dataAPI用）。ここでは"Web"としておきます。

### WebViewerの設定
WebViewer内の計算ボックスに直接コードを記述はしません。以下のように計算ボックは空白で、チェックボックスの状態はこのように
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/171037/1433c4f8-2dbe-5449-02b6-a7da827ff503.png)

### WebViewerを設定スクリプトステップ
```
変数を設定　$html　（上で設定したcodeテーブルのターゲットレコードのhtmlフィールドの内容を取得して格納。callNameを利用してExecuteSql関数などで取得してくる）
変数を設定　$objectName : "web"
WebViewerを設定　（以下のスクショのように、URLを移動で、計算式の中は$html）
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/171037/6251e2e0-b68d-6764-7dc0-61c9faf13bc2.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/171037/933925fb-0e43-c90e-b446-47c9b9d997fc.png)

WebViewerの中の計算式に直接htmlを書くのが反応としては一番早いのですが、柔軟性がないので、このようにレコードに保管したコードを変数に入れて、"urlへ移動"で呼び出します。

### おわりに

Webに馴染みのない方にはちょっとわかりにくくて申し訳ないですが、やっている事は単純でdataAPIを使ってFileMakerのレコードを更新するだけです。とても単純な事で、実際にやっておられる方もいらっしゃるはずなのでここに書くのも憚られますが、個人的には劇的に楽になったのでアドベントカレンダーネタとして書きました。

WebViewerとレコードとの繋ぎの部分は、ブラウザやOSなどFileMaker以外の環境に依存する部分があり、実はかなりわかりにくく、難しい箇所だと常々思っています。また、ここで詰まる人も多いと認識しています。自分も何度経験しても新たな問題に遭遇したりします。レコードとWebViewer間では相互にコードを叩けるようになったので、後はこの部分だけスムーズになればもっと明るい未来になると思います。