Executables(実行可能関数)
バックエンド関数をクライアントに公開し、サーバーリソースへフルアクセスできます。
なぜ Executables を使うのか
フロントエンドが、サーバー側リソースを必要とする処理を行う必要がある場合があります。たとえば、秘密の 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 は、クライアントから直接呼び出せるバックエンド関数です。シークレット、データベース、外部 API、複雑なビジネスロジックなどのサーバー側リソースが必要な操作に対して、シンプルな RPC スタイルのインターフェースを提供します。
いつ executables を使うべきか
| ユースケース | 推奨 |
|---|---|
| クライアントからサーバーサイドロジックを持つ関数を呼び出す | ✅ Executable |
| データベースの変更に反応する | Triggers を使用 |
| スケジュールでコードを実行する | Schedulers を使用 |
| 外部サービス向けに HTTP エンドポイントを公開する | Webhooks を使用 |
| リアルタイムのデータ同期 | Database を直接使用 |
仕組み
SquidServiceを継承したクラス内のメソッドに@executable()を付与します- Squid がデプロイ時に関数を検出して登録します
- クライアントが
squid.executeFunction('functionName', ...args)で関数を呼び出します - バックエンドが、シークレット、コンテキスト、インテグレーションへのフルアクセスで関数を実行します
- 結果がシリアライズされ、クライアントへ返されます
クイックスタート
前提条件
squid init-backendで初期化された Squid backend プロジェクト@squidcloud/backendパッケージがインストールされていること
Step 1: 実行可能関数(executable function)を作成する
SquidService を継承する service クラスを作り、executable function を追加します。
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 ファイルから service が export されていることを確認します。
export * from './example-service';
Step 3: backend をデプロイする
executable を利用可能にするために backend をデプロイします。
squid deploy
Step 4: クライアントから呼び出す
const greeting = await squid.executeFunction('greet', 'World');
console.log(greeting); // Output: "Hello, World!"
認証(Authentication)と認可(Authorization)
Executables は、シークレット、データベース、インテグレーションを含むバックエンドリソースへ 無制限にアクセス できます。呼び出し元が要求されたアクションを実行する権限を持つことを、必ず検証してください。
リソースへのアクセスを自動的に保護する 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';
}
}
認証メソッドやバックエンドを安全に保つ方法の詳細は、Security rules を参照してください。
コアコンセプト
リクエストコンテキスト
すべての 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();
}
| プロパティ | 型 | 説明 |
|---|---|---|
this.secrets | Record<string, SecretValue> | Console 由来の secrets の key-value map。SecretValue は string | number | boolean。 |
this.apiKeys | Record<string, string> | API keys の key-value map |
ファイルアップロード(SquidFile)
Executables はクライアントからアップロードされたファイルを受け取れます。パラメータとして送られたファイルは自動的に SquidFile オブジェクトへ変換されます。
Client-side:
// 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:
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}`);
}
}
SquidFile properties:
| プロパティ | 型 | 説明 |
|---|---|---|
data | Uint8Array | バイナリファイル内容 |
originalName | string | アップロード時の元ファイル名 |
mimetype | string | MIME type(例: 'image/png') |
size | number | ファイルサイズ(バイト) |
fieldName | string | フォームフィールド名 |
encoding | string | undefined | 指定されている場合のファイルエンコーディング |
Squid client を使う
executable 内から this.squid を使って他の Squid services にアクセスできます。これにより、client SDK で利用できる同じ Database と storage APIs にアクセスできます。
@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 デコレーターを使って、executables の悪用を防げます。レート制限(秒あたりのクエリ数)とクォータ制限(期間あたりの総呼び出し数)を定義でき、グローバル、ユーザー単位、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 error を 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 のしきい値に到達 | backoff を使ったリトライを実装し、limits を調整 |
Network error | 接続性の問題 | リトライロジックを実装 |
ベストプラクティス
セキュリティ
- センシティブな操作では 常に認証を検証 する
- 処理前に すべての入力パラメータを検証 する
- クライアントに 内部エラーの詳細を絶対に露出しない
- 公開される executable には レート制限 を使う
- 種類・サイズ・内容をチェックして ファイルアップロードをサニタイズ する
@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 を有効にする
- 必要なデータだけを返して payload を小さく保つ
- 長時間実行タスクでは 重い処理を schedulers や queue にオフロード する
命名規約
- 関数名は camelCase を使用
- 説明的な動詞 を使う(例:
createOrder,updateProfile,deleteDocument) - 関連する関数は同じ service class にグルーピングする
コード例
データベース操作
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 };
}
}
他のデータベース操作(queries, transactions, joins)については、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 };
}
}
関連項目
- Triggers - データベースの変更に反応する
- Schedulers - スケジュールでコードを実行する
- Webhooks - HTTP エンドポイントを公開する
- Rate and quota limiting - バックエンド関数を保護する
- Security Rules - バックエンドを安全に保つ