コンテンツにスキップ

ファンクションを書く

ファンクションのスクリプトは、スクリプトノードと同じく TypeScript で書き、ランナー上の Deno で実行されます。環境変数・認証情報・データベースなどの synqlet: モジュールも、スクリプトノードと同じように import できます(スクリプトを書く を参照)。

このページでは、ファンクションならではの書き方と、よく使う処理の例を紹介します。

ファンクションは、呼び出されたときに実行する関数を default export します。

/**
* 税込み価格を計算する
*/
export default function (price: number, taxRate = 0.1): number {
return Math.floor(price * (1 + taxRate));
}

スクリプトノードと違い、引数と戻り値の型は自由です(./nodeType.ts の Parameter / Result はありません)。呼び出し側に見せたい形で引数を定義し、戻り値の型を書いてください。書いた型は、呼び出す側のエディタでも補完に使われます。

fetch などの非同期処理を行う場合は async function にして Promise を返します。

フローのスクリプトノードでは、synqlet:functions/{ファンクションのID または コード} を import して、普通の関数として呼び出します。非同期のファンクションは await してください。

import { Parameter, Result } from "./nodeType.ts";
import calcTaxIncluded from "synqlet:functions/calc_tax_included";
export default async function (parameter: Parameter): Promise<Result> {
const price = calcTaxIncluded(1000);
return { price };
}

ファンクションを作成すると、fetch で外部 API を呼び出し、自分の IP アドレスを返すサンプルが入っています。先頭のコメントには、作成時のファンクション名が入ります。プレイグラウンドとテストコードも、このサンプルに合わせた形で入っているので、そのまま実行して動きを確かめられます(プレイグラウンドで試す)。

/**
* {ファンクション名}
*/
export default async function (): Promise<string> {
// 自分自身のIPアドレスを取得するサンプル
// ファンクションのスクリプト例は https://help.synqlet.com/features/functions/scripts/ を参考にしてください
console.log("自分自身のIPアドレスを取得します");
const response = await fetch("https://get.geojs.io/v1/ip/geo.json");
if (!response.ok) {
throw new Error("API Error");
}
const geo: { ip: string } = await response.json();
console.log(geo);
return geo.ip;
}

例: 外部 API を呼び出す(fetch)

Section titled “例: 外部 API を呼び出す(fetch)”

HTTP の API は、Deno に標準で入っている fetch で呼び出せます。専用の HTTP クライアントのパッケージを入れる必要はありません。

接続先の URL は環境変数に、アクセストークンは認証情報に置いておくと、スクリプトに秘密の値を書かずに済みます。

import API_BASE_URL from "synqlet:environment-variables/API_BASE_URL";
import credential from "synqlet:credentials/SAMPLE_API_TOKEN";
type Customer = {
id: string;
name: string;
};
/**
* 顧客を1件取得する
*/
export default async function (customerId: string): Promise<Customer> {
const response = await fetch(
new URL(`/customers/${encodeURIComponent(customerId)}`, API_BASE_URL),
{
headers: { Authorization: `Bearer ${await credential()}` },
},
);
if (!response.ok) {
// 失敗の理由がログに残るように、ステータスと本文をエラーに含める
throw new Error(
`顧客の取得に失敗しました: ${response.status} ${await response.text()}`,
);
}
return await response.json();
}
  • fetch は HTTP のエラー(404 や 500)でも例外を投げません。response.ok を必ず確かめてください。
  • await credential() は、外部サービスを呼ぶ直前に毎回呼んでください(取得した値はキャッシュされます)。

JSON を送るときは body に文字列を渡し、Content-Type を付けます。

const response = await fetch(new URL("/customers", API_BASE_URL), {
method: "POST",
headers: {
Authorization: `Bearer ${await credential()}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ name: "株式会社サンプル" }),
});

npm と JSR のパッケージは、npm: / jsr: を付けて import するだけで使えます(事前のインストールは不要です)。エディタ上部の「外部パッケージ」の「+」から追加することもできます。

import { WebClient } from "npm:@slack/web-api@8.1.1";
import { parse } from "jsr:@std/csv@1.0.6";

import には、@ の後ろにバージョンを 1.0.6 の形まで書いて固定することを強くおすすめします。

バージョンを書かないと、実行するたびにその時点の最新版が使われます。パッケージの新しい版が公開されただけで、スクリプトを何も変えていないのに動きが変わったり、動かなくなったりします。@4 のようにメジャーバージョンだけを書いた場合も、その範囲の最新版に上がっていきます。

例: CSV を読み書きする(jsr:@std/csv)

Section titled “例: CSV を読み書きする(jsr:@std/csv)”

Deno の標準ライブラリ jsr:@std/csv を使うと、CSV の解析と生成ができます。

import { parse, stringify } from "jsr:@std/csv@1.0.6";
type Score = {
name: string;
score: number;
};
/**
* CSV の文字列を読み取り、点数の高い順に並べ替えた CSV を返す
*/
export default function (csv: string): string {
// 1行目を列名として使い、各行をオブジェクトとして受け取る
const rows = parse(csv, { skipFirstRow: true });
const scores: ReadonlyArray<Score> = rows
.map((row) => ({ name: row.name, score: Number(row.score) }))
.toSorted((a, b) => b.score - a.score);
return stringify(scores, { columns: ["name", "score"] });
}

大きなファイルを1行ずつ処理したいときは、CsvParseStream / CsvStringifyStream でストリームのまま変換できます。例はファイルを扱うを参照してください。

例: Slack にメッセージを投稿する(npm:@slack/web-api)

Section titled “例: Slack にメッセージを投稿する(npm:@slack/web-api)”

npm のパッケージも同じように使えます。次は Slack 公式の @slack/web-api で、チャンネルにメッセージを投稿する例です。通知の処理をファンクションにまとめておくと、複数のフローから同じ書き方で通知できます。

Slack アプリの Bot トークン(xoxb- で始まるもの)は、認証情報に登録しておきます。

import { WebClient } from "npm:@slack/web-api@8.1.1";
import slackBotToken from "synqlet:credentials/SLACK_BOT_TOKEN";
/**
* Slack のチャンネルにメッセージを投稿し、投稿のタイムスタンプを返す
*/
export default async function (channel: string, text: string): Promise<string> {
// 認証情報は呼び出すたびに取り出す(取り出した値はキャッシュされる)
const client = new WebClient(await slackBotToken());
// 投稿に失敗すると例外になる(チャンネルが見つからない・Bot が参加していない など)
const result = await client.chat.postMessage({ channel, text });
if (!result.ts) {
throw new Error("Slack への投稿結果にタイムスタンプがありません");
}
return result.ts;
}
  • フローから呼び出すときは、呼び出すスクリプトノードのセキュリティフラグで次の2つを許可してください(権限について)。
    • ネットワークアクセス: slack.com
    • システム情報: osRelease(@slack/web-api は読み込むときに OS のバージョンを読むため、これが無いと NotCapable: Requires sys access to "osRelease" で止まります)
  • Bot をチャンネルに招待しておかないと、not_in_channel のエラーになります。

他のファンクションを呼び出す

Section titled “他のファンクションを呼び出す”

ファンクションの中からも、synqlet:functions/... で他のファンクションを呼び出せます。小さなファンクションを組み合わせて使うと、それぞれを個別に試せるので保守しやすくなります。

import calcTaxIncluded from "synqlet:functions/calc_tax_included";
import postToSlack from "synqlet:functions/post_to_slack";
/**
* 注文の税込み金額を計算して Slack に通知する
*/
export default async function (
orderId: string,
price: number,
): Promise<number> {
const total = calcTaxIncluded(price);
await postToSlack(
"#orders",
`注文 ${orderId} を受け付けました(税込み ${total} 円)`,
);
return total;
}

ファンクションの詳細画面では、実行するランナーを選んでプレイグラウンド(試し実行用のスクリプト)を実行できます。プレイグラウンドでは ./function.ts から編集中のスクリプトを import して呼び出します。

import main from "./function.ts";
const result = await main("019cd0a6-ea24-7677-87af-6f62f67553f0");
console.log(result);

保存していない編集中のスクリプトも、そのまま試せます。ログや結果は画面右のパネルに表示されます。

テストコードには、deno test で実行する自動テストを書けます。jsr:@std/assert で期待する値と比べます。

次は基本の形の「税込み価格を計算する」ファンクションのテストです。

import { assertEquals } from "jsr:@std/assert@1.0.19";
import main from "./function.ts";
Deno.test("税込み価格を計算する", () => {
assertEquals(main(1000), 1100);
assertEquals(main(1000, 0.08), 1080);
});

次はCSV を読み書きするファンクションのテストです。

import { assertEquals } from "jsr:@std/assert@1.0.19";
import main from "./function.ts";
Deno.test("点数の高い順に並べ替える", () => {
const csv = "name,score\r\n佐藤,70\r\n鈴木,90\r\n";
assertEquals(main(csv), "name,score\r\n鈴木,90\r\n佐藤,70\r\n");
});
  • 例外になることを確かめるには assertThrows を使います。
  • 非同期のファンクションは、Deno.test に async の関数を渡して await します(例外を確かめるときは assertRejects)。
  • Slack への投稿のように外部サービスへ実際に送ってしまう処理は、テストコードでは呼ばず、プレイグラウンドで確かめるのが安全です。

権限(セキュリティフラグ)について

Section titled “権限(セキュリティフラグ)について”

ファンクション自体には、セキュリティフラグの設定はありません。

  • プレイグラウンド・テストコードの実行では、すべての権限が許可された状態で実行されます。
  • フローから呼び出したときは、呼び出したスクリプトノードのセキュリティフラグが適用されます。ファンクションの中で fetch するなら、呼び出す側のスクリプトノードで「ネットワークアクセス」を許可してください。→ セキュリティフラグ

認証情報・データベースを使うファンクションを公開するとき

Section titled “認証情報・データベースを使うファンクションを公開するとき”

ファンクションの中で認証情報やデータベースを import すると、そのファンクションを呼び出しているすべてのフローが、次の実行から認証情報・データベースを使うようになります。すでに使われているファンクションに追加してリビジョンをアクティブにする前に、次を確かめてください。

  • ランナーに秘密鍵ファイルがあるか — 認証情報の復号には、フローを実行するランナーに秘密鍵ファイルが必要です。置いていないランナーで動いているフローは、認証情報を取り出せずに失敗します。→ 認証情報
  • 呼び出す側の権限が足りているか — ファンクションの中から新しい接続先へ fetch するなら、呼び出しているすべてのスクリプトノードで、その接続先へのネットワークアクセスを許可する必要があります。Synqlet 自身への通信(認証情報の取得やデータベースの読み書き)は自動で許可されます。

プレイグラウンドとテストコードはすべての権限が許可された状態で実行されるため、ここで動いてもフローで動くとは限りません。フローのプレイグラウンドで、呼び出す側のスクリプトノードを試しておくと確実です。