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

Executables

server resource への完全な access を持つ backend function を client に公開します。​

Executables を使用する理由​

frontend で、secret API key へのアクセス、database の query、browser に公開したくない business logic の実行など、server-side resource を必要とする処理を実行する必要があります。

executables を使用しない場合、route の定義、serialization の処理、CORS の管理、個別 server の deploy を含む API layer 全体をセットアップする必要があります。executables では、function を記述して呼び出すだけです。

// 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);

route は不要です。API boilerplate も不要です。必要なのは function だけです。

概要​

Executables は、client から直接呼び出せる backend function です。secret、database、external API、複雑な business logic など、server-side resource を必要とする operation 向けのシンプルな RPC-style interface を提供します。

Executables を使用する場合​

ユースケース推奨
server-side logic を持つ function を client から呼び出す✅ Executable
database の変更に反応するTriggers を使用
schedule に沿って code を実行するSchedulers を使用
external service に HTTP endpoint を公開するWebhooks を使用
real-time data synchronizationDatabase を直接使用

仕組み​

  1. SquidService を拡張する class 内で、method を @executable() で decorate します
  2. Squid が deploy 時に function を検出・登録します
  3. client は squid.executeFunction('functionName', ...args) を使用して function を呼び出します
  4. backend は secret、context、integration への完全な access を持って function を実行します
  5. result は serialize され、client に返されます

クイックスタート​

前提条件​

  • squid init で初期化された Squid backend project
  • NPM からインストールされた @squidcloud/backend package

ステップ 1: executable function を作成する​

SquidService を拡張する service class を作成し、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}!`;
}
}

ステップ 2: service を export する​

service が service index file から export されていることを確認します。

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

ステップ 3: backend を開始または deploy する​

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

squid start

cloud に deploy するには、backend の deployを参照してください。

ステップ 4: client から呼び出す​

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

Authentication と Authorization​

Security Warning

Executables は、secret、database、integration を含む backend resource に無制限に accessできます。caller が要求された action を実行する権限を持つことを常に validate してください。

resource への access を自動的に保護する security rules とは異なり、executables では function code 内で authentication を手動で確認する必要があります。

Authentication の確認​

authentication を要求するには 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';
}
}

authentication method と backend の保護に関する詳細は、backend で auth を使用するを参照してください。

コアコンセプト​

Request context​

すべての executable は this.context を介して request context に access できます。

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)
}
PropertyType説明
appIdstringapplication ID
clientIdstring | undefined呼び出し元 client の unique identifier
sourceIpstring | undefinedcaller の IP address
headersRecord<string, any> | undefinedHTTP header(key は lowercase)

Secrets と API keys​

Squid Console で定義したapplication secretに access します。

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

File upload(SquidFile)​

Executables は client から upload された file を受け取れます。parameter として送信された file は、自動的に SquidFile object に変換されます。

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 の使用​

this.squid を使用して executable 内から他の Squid service に access します。これにより、client SDK で利用可能な同じ Database および storage API に access できます。

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 operation の詳細は、Database documentationを参照してください。

Client-Side Advanced Options​

Custom headers​

this.context.headers から access できる custom header を送信します。

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
// ...
}

executeFunctionWithHeaders は executeFunction と同じ signature を持ちますが、function parameter の前、2 番目の argument として headers object が挿入されます。留意すべき動作は以下のとおりです。

  • header 名は送信時に lowercase 化されるため、this.context.headers では lowercase key を使用して読み取ります。
  • custom header は Squid 独自の request header(authorization など)と merge され、置き換えることはありません。
  • executeFunction は、empty headers object を指定した executeFunctionWithHeaders と同等です。
  • custom header は TypeScript Client SDK で利用できます。Python client の execute_function は header を受け取りません。

Result の Caching​

冗長な request を避けるため、client で高コストな function call を cache します。

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

Concurrent call の Deduplication​

同じ argument による concurrent request の重複を防止します。

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 を使用して executable を不正利用から保護します。global、user ごと、IP address ごとの scope で、rate limit(1 秒あたりの query 数)と quota limit(期間あたりの合計 call 数)を定義できます。

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> {
// ...
}
}

scope option、enforcement behavior、renewal period を含む rate と quota limiting の詳細は、Rate and quota limitingを参照してください。

Error Handling​

Error の Throw​

標準 JavaScript error を throw します。これらは serialize され、client に返されます。

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 での Error の処理​

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
}

一般的な Error​

Error原因解決策
Function not foundfunction 名がどの @executable とも一致しないspelling を確認し、service が export されていることを確認する
UNAUTHORIZEDassertIsAuthenticated() が失敗した呼び出し前に user が log in していることを確認する
Rate limit exceeded@limits threshold に到達したbackoff を伴う retry を実装するか、limit を調整する
Network errorconnectivity issueretry logic を実装する

Best Practices​

Security​

  1. sensitive operation に対しては常に authentication を validate する
  2. 処理前にすべての input parameter を validate する
  3. internal error detail を client に公開しない
  4. public-facing executable ではrate limiting を使用する
  5. type、size、content を確認して、file upload を sanitize する
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');
}
}

Performance​

  1. 高コストで idempotent な operation にはclient-side caching を使用する
  2. 冗長な concurrent call を防止するためdeduplication を有効にする
  3. 必要な data のみを返し、payload を小さく保つ
  4. 長時間実行 task の heavy processing は、scheduler または queue にoffload する

Naming conventions​

  • function 名には camelCase を使用します
  • 説明的な verb を使用します(例: createOrder、updateProfile、deleteDocument)
  • 関連する function を同じ service class に group 化します

コード例​

Database operation​

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

query、transaction、join など、その他の database operation については、Database documentationを参照してください。

External 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();
}
}

File upload の処理​

この例では、upload された file を validate し、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 };
}
}

関連項目​