メインコンテンツまでスキップ

Executables(実行可能関数)

サーバーリソースへフルアクセスできるバックエンド関数をクライアントに公開します。

Executables を使う理由

フロントエンドで、サーバー側リソースが必要な処理(secret な API key へのアクセス、データベースのクエリ、ブラウザに公開したくないビジネスロジックの実行)を行いたい場合があります。

Executables がないと、API レイヤー全体を用意する必要があります。つまり、ルート定義、シリアライズ処理、CORS 管理、別サーバーのデプロイなどです。Executables なら、関数を書いて呼び出すだけです。

// Backend: just a decorated method
@executable()
async processPayment(orderId: string, amount: number): Promise<Receipt> {
const apiKey = this.secrets['PAYMENT_API_KEY']; // Access secrets securely
return await paymentService.charge(orderId, amount, apiKey);
}

// Frontend: call it like a local function
const receipt = await squid.executeFunction('processPayment', orderId, 99.99);

ルート不要。API のボイラープレート不要。関数だけ。

概要

Executables は、クライアントから直接呼び出せるバックエンド関数です。secrets、データベース、外部 API、複雑なビジネスロジックなど、サーバー側リソースが必要な操作に対して、シンプルな RPC 風インターフェースを提供します。

Executables を使うべきとき

ユースケース推奨
サーバー側ロジックを伴う関数をクライアントから呼ぶ✅ Executable
データベースの変更に反応するTriggers を使用
スケジュールでコードを実行するSchedulers を使用
外部サービス向けに HTTP エンドポイントを公開するWebhooks を使用
リアルタイムのデータ同期Database を直接使用

仕組み

  1. SquidService を継承したクラス内のメソッドに @executable() を付与します
  2. Squid が deploy 時に関数を検出して登録します
  3. クライアントが squid.executeFunction('functionName', ...args) で関数を呼び出します
  4. バックエンドが secrets、context、integration へのフルアクセスで関数を実行します
  5. 結果がシリアライズされてクライアントに返されます

クイックスタート

前提条件

  • squid init で初期化された Squid backend プロジェクト
  • @squidcloud/backend パッケージがNPMからインストールされていること

Step 1: executable 関数を作成する

SquidService を継承する service クラスを作成し、executable 関数を追加します。

Backend code
import { executable, SquidService } from '@squidcloud/backend';

export class ExampleService extends SquidService {
@executable()
async greet(name: string): Promise<string> {
return `Hello, ${name}!`;
}
}

Step 2: service をエクスポートする

service の index ファイルからエクスポートされていることを確認します。

service/index.ts
export * from './example-service';

Step 3: backend を起動または deploy する

ローカル開発では、Squid CLI を使って backend をローカルで実行します。

squid start

クラウドへ deploy する場合は、deploying your backend を参照してください。

Step 4: クライアントから呼び出す

Client code
const greeting = await squid.executeFunction('greet', 'World');
console.log(greeting); // Output: "Hello, World!"

認証と認可

Security Warning

Executables は secrets、データベース、integration を含むバックエンドリソースに 無制限にアクセス できます。呼び出し元が要求されたアクションを実行する権限を持つかどうかを、必ず検証してください。

リソースへのアクセスを自動的にガードする security rules と異なり、Executables は関数コード内で手動で認証チェックを行う必要があります。

認証の確認

認証を必須にするには this.assertIsAuthenticated()(未認証なら 'UNAUTHORIZED' を throw)を使います。手動でチェックするには this.isAuthenticated() を使います。

Backend code
import { executable, SquidService } from '@squidcloud/backend';

export class SecureService extends SquidService {
@executable()
async getSecretData(): Promise<string> {
// Throws UNAUTHORIZED if not authenticated
this.assertIsAuthenticated();

// Access user details for authorization decisions
const userAuth = this.getUserAuth();
if (!userAuth?.attributes?.['role']?.includes('admin')) {
throw new Error('Admin access required');
}

return 'Secret data';
}
}

認証メソッドや backend の保護についての詳細は、using auth in the backend を参照してください。

コアコンセプト

リクエスト context

すべての executable は this.context 経由で request context にアクセスできます。

Backend code
@executable()
async logRequestInfo(): Promise<void> {
const ctx = this.context;

console.log('App ID:', ctx.appId);
console.log('Client ID:', ctx.clientId); // Unique client identifier
console.log('Source IP:', ctx.sourceIp); // Client IP address
console.log('Headers:', ctx.headers); // Request headers (lowercase keys)
}
プロパティ説明
appIdstringあなたの application ID
clientIdstring | undefined呼び出し元 client の一意な識別子
sourceIpstring | undefined呼び出し元の IP アドレス
headersRecord<string, any> | undefinedHTTP headers(キーは小文字)

Secrets と API keys

Squid Console で定義した application secrets にアクセスします。

Backend code
@executable()
async callExternalApi(): Promise<any> {
const apiKey = this.secrets['EXTERNAL_API_KEY'];

const response = await fetch('https://api.example.com/data', {
headers: { 'Authorization': `Bearer ${apiKey}` }
});

return response.json();
}

ファイルアップロード(SquidFile)

Executables は、クライアントからアップロードされたファイルを受け取れます。パラメータとして送信されたファイルは、自動的に SquidFile オブジェクトへ変換されます。

Client-side:

Client code
// Single file
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
const file = fileInput.files[0];
const result = await squid.executeFunction('uploadDocument', file, 'My Document');

// Multiple files
const files = Array.from(fileInput.files);
const result = await squid.executeFunction('uploadDocuments', files);

Backend:

Backend code
import { executable, SquidService } from '@squidcloud/backend';
import { SquidFile } from '@squidcloud/backend';

export class FileService extends SquidService {
@executable()
async uploadDocument(file: SquidFile, title: string): Promise<string> {
console.log('Original filename:', file.originalName);
console.log('MIME type:', file.mimetype);
console.log('Size (bytes):', file.size);

// Access file content as Uint8Array
const content = file.data;

// Process the file...
return `Uploaded: ${title} (${file.size} bytes)`;
}

@executable()
async uploadDocuments(files: SquidFile[]): Promise<string[]> {
return files.map((f) => `Processed: ${f.originalName}`);
}
}

Squid client を使う

executable 内から this.squid を使って他の Squid サービスへアクセスできます。これにより、client SDK で利用できるものと同じ Database および storage API にアクセスできます。

Backend code
@executable()
async createUserWithData(userData: UserData): Promise<void> {
const usersCollection = this.squid.collection<User>('users');
const userRef = usersCollection.doc(userData.id);
await userRef.insert(userData);
}

データベース操作についての詳細は、Database documentation を参照してください。

クライアント側の高度なオプション

カスタム headers

this.context.headers から参照できるカスタム headers を送信します。

Client code
const result = await squid.executeFunctionWithHeaders(
'processOrder',
{
'x-idempotency-key': 'order-123-attempt-1',
'x-client-version': '2.0.0',
},
orderData
);
Backend code
@executable()
async processOrder(orderData: OrderData): Promise<Order> {
const idempotencyKey = this.context.headers?.['x-idempotency-key'];
const clientVersion = this.context.headers?.['x-client-version'];

// Use idempotency key to prevent duplicate processing
// ...
}

結果のキャッシュ

高コストな関数呼び出しをクライアント側でキャッシュし、冗長なリクエストを避けます。

Client code
import { LastUsedValueExecuteFunctionCache } from '@squidcloud/client';

// Create a cache that stores results for 5 minutes
const weatherCache = new LastUsedValueExecuteFunctionCache<WeatherData>({
valueExpirationMillis: 5 * 60 * 1000,
});

// Use the cache
const weather = await squid.executeFunction(
{
functionName: 'getWeather',
caching: { cache: weatherCache },
},
'New York'
);

同時呼び出しの重複排除(Deduplicating)

同じ引数での重複した同時リクエストを防ぎます。

Client code
// Using default reference comparison
const result = await squid.executeFunction(
{
functionName: 'expensiveCalculation',
deduplication: true,
},
inputData
);

// Using serialized value comparison (for object arguments)
import { compareArgsBySerializedValue } from '@squidcloud/client';

const result = await squid.executeFunction(
{
functionName: 'expensiveCalculation',
deduplication: { argsComparator: compareArgsBySerializedValue },
},
inputData
);

レート制限(Rate Limiting)

@limits decorator を使って abuse から executables を保護します。レート制限(1 秒あたりのクエリ数)とクォータ制限(期間あたりの総呼び出し数)を定義でき、グローバル、ユーザー単位、IP アドレス単位でスコープできます。

Backend code
import { executable, limits, SquidService } from '@squidcloud/backend';

export class RateLimitedService extends SquidService {
@executable()
@limits({
rateLimit: 5,
quotaLimit: { value: 100, scope: 'user', renewPeriod: 'monthly' },
})
async limitedAction(): Promise<void> {
// ...
}
}

スコープ指定、適用(enforcement)の挙動、更新期間(renewal periods)など、レート制限とクォータ制限の詳細は、Rate and quota limiting を参照してください。

エラーハンドリング

エラーを throw する

標準の JavaScript error を throw できます。これらはシリアライズされ、クライアントに返されます。

Backend code
@executable()
async riskyOperation(data: InputData): Promise<Result> {
if (!data.requiredField) {
throw new Error('requiredField is missing');
}

try {
return await this.performOperation(data);
} catch (error) {
// Log server-side for debugging
console.error('Operation failed:', error);

// Throw a user-friendly message
throw new Error('Operation failed. Please try again.');
}
}

クライアント側でのエラー処理

Client code
try {
const result = await squid.executeFunction('riskyOperation', data);
console.log('Success:', result);
} catch (error) {
console.error('Function failed:', error.message);
// Handle the error appropriately
}

よくあるエラー

エラー原因解決策
Function not found関数名がどの @executable とも一致しないスペルを確認; service が export されているか確認
UNAUTHORIZEDassertIsAuthenticated() が失敗した呼び出し前にユーザーがログインしていることを確認
Rate limit exceeded@limits の閾値に達したbackoff 付きリトライを実装; limits を調整
Network error接続性の問題リトライロジックを実装

ベストプラクティス

セキュリティ

  1. 機密性の高い操作では 必ず認証を検証する
  2. 処理前に すべての入力パラメータを検証する
  3. 内部エラーの詳細をクライアントに公開しない
  4. 公開(public-facing)executables には レート制限を使用する
  5. タイプ、サイズ、内容をチェックして ファイルアップロードをサニタイズする
Backend code
@executable()
@limits({
rateLimit: 10,
quotaLimit: { value: 100, scope: 'user', renewPeriod: 'monthly' },
})
async secureAction(input: UserInput): Promise<Result> {
// 1. Authenticate
this.assertIsAuthenticated();

// 2. Validate input
if (!input || typeof input.value !== 'string' || input.value.length > 1000) {
throw new Error('Invalid input');
}

// 3. Authorize (check permissions)
const user = this.getUserAuth();
if (!user?.attributes?.['canPerformAction']) {
throw new Error('Permission denied');
}

// 4. Execute with error handling
try {
return await this.doAction(input);
} catch (error) {
console.error('Action failed:', error);
throw new Error('Action failed');
}
}

パフォーマンス

  1. 高コストで冪等(idempotent)な操作には クライアント側キャッシュ を使う
  2. 冗長な同時呼び出しを防ぐため deduplication を有効化する
  3. 必要なデータのみ返して payload を小さく保つ
  4. 長時間タスクは schedulers や queues に 重い処理をオフロード する

命名規約

  • 関数名には camelCase を使う
  • 説明的な動詞 を使う(例: createOrder, updateProfile, deleteDocument
  • 関連する関数は同じ service class にまとめる

コード例

データベース操作

Backend code
import { executable, SquidService } from '@squidcloud/backend';

interface Product {
id: string;
name: string;
price: number;
stock: number;
}

export class InventoryService extends SquidService {
@executable()
async updateStock(productId: string, quantity: number): Promise<Product> {
this.assertIsAuthenticated();

const products = this.squid.collection<Product>('products');
const productRef = products.doc(productId);

const product = await productRef.snapshot();
if (!product) {
throw new Error(`Product ${productId} not found`);
}

const newStock = product.stock + quantity;
if (newStock < 0) {
throw new Error('Insufficient stock');
}

await productRef.update({ stock: newStock });

return { ...product, stock: newStock };
}
}

クエリ、トランザクション、join など、さらに多くのデータベース操作については、Database documentation を参照してください。

外部 API の呼び出し

Backend code
import { executable, SquidService } from '@squidcloud/backend';

interface WeatherData {
temperature: number;
conditions: string;
}

export class WeatherService extends SquidService {
@executable()
async getWeather(city: string): Promise<WeatherData> {
const apiKey = this.secrets['WEATHER_API_KEY'];

const response = await fetch(`https://api.weather.example.com/v1/current?city=${encodeURIComponent(city)}`, {
headers: { 'X-API-Key': apiKey },
});

if (!response.ok) {
throw new Error(`Weather API error: ${response.status}`);
}

return response.json();
}
}

ファイルアップロードの処理

この例は、アップロードされたファイルを検証し、Squid storage を使って保存する方法を示します。

Backend code
import { executable, SquidService } from '@squidcloud/backend';
import { SquidFile } from '@squidcloud/backend';

export class ImageService extends SquidService {
@executable()
async processImage(image: SquidFile): Promise<{ id: string; url: string }> {
this.assertIsAuthenticated();

// Validate file type
if (!image.mimetype.startsWith('image/')) {
throw new Error('Only image files are allowed');
}

// Validate file size (max 5MB)
const maxSize = 5 * 1024 * 1024;
if (image.size > maxSize) {
throw new Error('File size exceeds 5MB limit');
}

// Store in Squid storage
const storage = this.squid.storage('images');
const imageId = crypto.randomUUID();
const dirPath = 'uploads';
const filePath = `${dirPath}/${imageId}-${image.originalName}`;

// Convert SquidFile to File for upload
const file = new File([image.data], image.originalName, { type: image.mimetype });
await storage.uploadFile(dirPath, file);

const { url } = await storage.getDownloadUrl(filePath);

return { id: imageId, url };
}
}

参照