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

実行可能関数(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 を直接使用

仕組み

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

クイックスタート

前提条件

  • squid init-backend で初期化された Squid backend プロジェクト
  • @squidcloud/backend パッケージがインストール済み

Step 1: 実行可能関数を作成する

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 を export する

service の index ファイルから export されていることを確認します。

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

Step 3: バックエンドを起動またはデプロイする

ローカル開発では、Squid CLI を使ってバックエンドをローカル実行します。

squid start

クラウドにデプロイするには、deploying your backend を参照してください。

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

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

認証(Authentication)と認可(Authorization)

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';
}
}

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

コアコンセプト

リクエストコンテキスト

すべての executable は this.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呼び出しクライアントの一意な識別子
sourceIpstring | undefined呼び出し元の IP アドレス
headersRecord<string, any> | undefinedHTTP ヘッダー(キーは小文字)

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 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 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 を参照してください。

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

カスタムヘッダー

this.context.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'
);

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

同じ引数で同時に発生する重複リクエストを防ぎます。

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 デコレーターを使用して abuse から executable を保護します。レート制限(queries per second)やクォータ制限(期間あたりの総呼び出し回数)を、グローバル、ユーザー単位、または 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 エラーを 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 の閾値に到達バックオフ付きリトライを実装; limits を調整
Network error接続性の問題リトライロジックを実装

ベストプラクティス

セキュリティ

  1. センシティブな操作では 必ず認証を検証する
  2. 処理前に すべての入力パラメータを検証する
  3. クライアントへ 内部エラーの詳細を公開しない
  4. 公開 executable には レート制限を使用する
  5. type、size、content をチェックして ファイルアップロードをサニタイズする
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. 必要なデータだけを返して ペイロードを小さく 保つ
  4. 長時間タスクは Schedulers やキューへオフロードして重い処理を避ける

命名規約

  • 関数名は camelCase を使用
  • 説明的な動詞 を使用(例: createOrder, updateProfile, deleteDocument
  • 関連する関数は同じ service クラスにグルーピングする

コード例

データベース操作

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 };
}
}

関連項目(See Also)