実行可能関数(Executables)
サーバーリソースへのフルアクセスを持つバックエンド関数をクライアントに公開します。
Executables を使う理由
フロントエンドが、サーバー側のリソースを必要とする処理を実行したいケースがあります。たとえば、秘密の API キーへのアクセス、データベースのクエリ、ブラウザに露出させたくないビジネスロジックの実行などです。
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 を直接使用 |
仕組み
SquidServiceを継承するクラス内で、メソッドに@executable()を付与します- Squid がデプロイ時に関数を検出して登録します
- クライアントは
squid.executeFunction('functionName', ...args)で関数を呼び出します - バックエンドは、secrets、context、integration へのフルアクセスを持って関数を実行します
- 結果はシリアライズされ、クライアントに返されます
クイックスタート
前提条件
squid init-backendで初期化された Squid backend プロジェクト@squidcloud/backendパッケージがインストール済み
Step 1: 実行可能関数を作成する
SquidService を継承する service クラスを作成し、executable 関数を追加します。
import { executable, SquidService } from '@squidcloud/backend';
export class ExampleService extends SquidService {
@executable()
async greet(name: string): Promise<string> {
return `Hello, ${name}!`;
}
}
Step 2: service を export する
service の index ファイルから export されていることを確認します。
export * from './example-service';
Step 3: バックエンドを起動またはデプロイする
ローカル開発では、Squid CLI を使ってバックエンドをローカル実行します。
squid start
クラウドにデプロイするには、deploying your backend を参照してください。
Step 4: クライアントから呼び出す
const greeting = await squid.executeFunction('greet', 'World');
console.log(greeting); // Output: "Hello, World!"
認証(Authentication)と認可(Authorization)
Executables は secrets、データベース、integration を含むバックエンドリソースへ 無制限にアクセス できます。呼び出し元が要求されたアクションを実行する権限を持つかどうかを、必ず検証してください。
リソースへのアクセスを自動的に保護する security rules と異なり、Executables では関数コード内で手動で認証チェックを行う必要があります。
認証の確認
認証必須にするには this.assertIsAuthenticated()(未認証の場合 'UNAUTHORIZED' を throw)を使います。手動チェックには this.isAuthenticated() を使います。
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';
}
}
認証メソッドやバックエンドの保護についての詳細は、using auth in the backend を参照してください。
コアコンセプト
リクエストコンテキスト
すべての executable は this.context を通じてリクエストコンテキストにアクセスできます。
@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)
}
| プロパティ | 型 | 説明 |
|---|---|---|
appId | string | あなたの application ID |
clientId | string | undefined | 呼び出しクライアントの一意な識別子 |
sourceIp | string | undefined | 呼び出し元の IP アドレス |
headers | Record<string, any> | undefined | HTTP ヘッダー(キーは小文字) |
Secrets と API keys
Squid Console で定義した application secrets にアクセスできます。
@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 オブジェクトへ変換されます。
クライアント側:
// 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);
バックエンド:
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 にアクセスできます。
@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 を参照してください。
クライアント側の高度なオプション
カスタムヘッダー
this.context.headers から参照できるカスタムヘッダーを送信します。
const result = await squid.executeFunctionWithHeaders(
'processOrder',
{
'x-idempotency-key': 'order-123-attempt-1',
'x-client-version': '2.0.0',
},
orderData
);
@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
// ...
}
結果のキャッシュ
高コストな関数呼び出しをクライアント側でキャッシュし、冗長なリクエストを避けます。
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'
);
同時呼び出しの重複排除(Deduplication)
同じ引数で同時に発生する重複リクエストを防ぎます。
// 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 デコレーターを使用して abuse から executable を保護します。レート制限(queries per second)やクォータ制限(期間あたりの総呼び出し回数)を、グローバル、ユーザー単位、または IP アドレス単位でスコープ指定して定義できます。
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 エラーを throw すると、シリアライズされてクライアントに返されます。
@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.');
}
}
クライアント側でエラーを扱う
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 されているか確認 |
UNAUTHORIZED | assertIsAuthenticated() が失敗 | 呼び出し前にユーザーがログインしていることを確認 |
Rate limit exceeded | @limits の閾値に到達 | バックオフ付きリトライを実装; limits を調整 |
Network error | 接続性の問題 | リトライロジックを実装 |
ベストプラクティス
セキュリティ
- センシティブな操作では 必ず認証を検証する
- 処理前に すべての入力パラメータを検証する
- クライアントへ 内部エラーの詳細を公開しない
- 公開 executable には レート制限を使用する
- type、size、content をチェックして ファイルアップロードをサニタイズする
@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');
}
}
パフォーマンス
- 高コストで冪等(idempotent)な操作には クライアント側キャッシュ を使用する
- deduplication を有効化して冗長な同時呼び出しを防ぐ
- 必要なデータだけを返して ペイロードを小さく 保つ
- 長時間タスクは Schedulers やキューへオフロードして重い処理を避ける
命名規約
- 関数名は camelCase を使用
- 説明的な動詞 を使用(例:
createOrder,updateProfile,deleteDocument) - 関連する関数は同じ service クラスにグルーピングする
コード例
データベース操作
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 の呼び出し
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 を使って保存する方法を示します。
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 };
}
}
関連項目(See Also)
- Triggers - データベース変更に反応
- Schedulers - スケジュール実行
- Webhooks - HTTP エンドポイントの公開
- Rate and quota limiting - バックエンド関数の保護
- Security Rules - バックエンドの保護