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

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 を直接使用

仕組み

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

クイックスタート

前提条件

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

Step 1: 実行可能関数(executable function)を作成する

SquidService を継承する service クラスを作り、executable function を追加します。

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 ファイルから service が export されていることを確認します。

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

Step 3: backend をデプロイする

executable を利用可能にするために backend をデプロイします。

squid deploy

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

Client code
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() で手動チェックできます。

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

認証メソッドやバックエンドを安全に保つ方法の詳細は、Security rules を参照してください。

コアコンセプト

リクエストコンテキスト

すべての 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();
}
プロパティ説明
this.secretsRecord<string, SecretValue>Console 由来の secrets の key-value map。SecretValuestring | number | boolean
this.apiKeysRecord<string, string>API keys の key-value map

ファイルアップロード(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}`);
}
}

SquidFile properties:

プロパティ説明
dataUint8Arrayバイナリファイル内容
originalNamestringアップロード時の元ファイル名
mimetypestringMIME type(例: 'image/png'
sizenumberファイルサイズ(バイト)
fieldNamestringフォームフィールド名
encodingstring | undefined指定されている場合のファイルエンコーディング

Squid client を使う

executable 内から this.squid を使って他の Squid services にアクセスできます。これにより、client SDK で利用できる同じ Database と storage APIs にアクセスできます。

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 デコレーターを使って、executables の悪用を防げます。レート制限(秒あたりのクエリ数)とクォータ制限(期間あたりの総呼び出し数)を定義でき、グローバル、ユーザー単位、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. 公開される executable には レート制限 を使う
  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 や queue にオフロード する

命名規約

  • 関数名は 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 };
}
}

他のデータベース操作(queries, transactions, joins)については、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 };
}
}

関連項目