yao/sui/docs/backend-scripts.md
Max a37bb5cdba Enhance API Guards and Documentation
- Added a new guard option for OAuth 2.1 authentication, with ACL checks performed in the Run function for API calls.
- Updated the API routing to include the new guard and clarified the OAuth guard's functionality in the code comments.
- Enhanced documentation to reflect the new guard options and their descriptions, improving clarity for developers on available authentication methods.
2026-01-04 16:40:23 +08:00

11 KiB

Backend Scripts

Backend scripts provide server-side logic for SUI pages, including data fetching, API endpoints, and helper functions.

File Naming

Backend scripts use the naming convention <page>.backend.ts or <page>.backend.js:

/users/list/
├── list.html
├── list.css
├── list.ts
└── list.backend.ts    # Backend script

Important Notes

⚠️ No ES Module Exports: Backend scripts do NOT support ES Module export syntax. Simply define functions directly - they will be automatically available based on naming conventions.

⚠️ $param Not Available: Unlike HTML templates, you cannot use $param.id directly in backend scripts. Route parameters must be accessed via the request.params object passed to your functions.

BeforeRender

The BeforeRender function is called before the page is rendered:

function BeforeRender(
  request: Request,
  props?: Record<string, any>
): Record<string, any> {
  return {
    user: Process("session.Get", "user"),
    items: Process("models.item.Get", { limit: 10 }),
  };
}

Parameters

  • request - The HTTP request object
  • props - Props passed when used as a component (optional)

Return Value

Return an object that will be merged with page data:

function BeforeRender(request: Request): Record<string, any> {
  const userId = request.query.userId;

  return {
    user: Process("models.user.Find", userId),
    posts: Process("models.post.Get", {
      wheres: [{ column: "user_id", value: userId }],
    }),
    stats: {
      views: 100,
      likes: 50,
    },
  };
}

API Methods

Functions prefixed with Api are exposed as callable endpoints. The backend automatically adds the Api prefix, so frontend calls use the method name without the prefix:

// Callable from frontend as: $Backend().Call("GetUsers")
function ApiGetUsers(request: Request): any[] {
  return Process("models.user.Get", {});
}

// Callable from frontend as: $Backend().Call("CreateUser", name, email)
function ApiCreateUser(name: string, email: string, request: Request): any {
  return Process("models.user.Create", { name, email });
}

// Callable from frontend as: $Backend().Call("DeleteUser", id)
function ApiDeleteUser(id: string, request: Request): boolean {
  Process("models.user.Delete", id);
  return true;
}

Calling from Frontend

import { $Backend, Component } from "@yao/sui";

const self = this as Component;

self.LoadUsers = async () => {
  // Call "ApiGetUsers" in backend script (without "Api" prefix)
  const users = await $Backend().Call("GetUsers");
  console.log(users);
};

self.CreateUser = async () => {
  const user = await $Backend().Call("CreateUser", "John", "john@example.com");
  console.log("Created:", user);
};

Constants

Export constants to the frontend using __sui_constants:

const __sui_constants = {
  API_URL: "/api/v1",
  MAX_ITEMS: 100,
  SUPPORTED_FORMATS: ["jpg", "png", "gif"],
  CONFIG: {
    timeout: 5000,
    retries: 3,
  },
};

Access in frontend:

import { Component } from "@yao/sui";

const self = this as Component;

console.log(self.constants.API_URL); // "/api/v1"
console.log(self.constants.MAX_ITEMS); // 100

Helpers

Export helper functions to the frontend using __sui_helpers:

const __sui_helpers = ["formatDate", "formatCurrency", "validateEmail"];

function formatDate(date: string): string {
  return new Date(date).toLocaleDateString();
}

function formatCurrency(amount: number, currency: string = "USD"): string {
  return new Intl.NumberFormat("en-US", {
    style: "currency",
    currency,
  }).format(amount);
}

function validateEmail(email: string): boolean {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}

Access in frontend:

import { Component } from "@yao/sui";

const self = this as Component;

const formatted = self.helpers.formatDate("2024-01-15");
const price = self.helpers.formatCurrency(99.99);
const isValid = self.helpers.validateEmail("test@example.com");

Request Object

The request object contains:

interface Request {
  method: string; // HTTP method
  url: {
    path: string;
    host: string;
    domain: string;
    scheme: string;
  };
  query: Record<string, string>; // Query parameters
  params: Record<string, string>; // Route parameters
  payload: Record<string, any>; // POST body
  headers: Record<string, string>; // HTTP headers
  sid: string; // Session ID
  theme: string; // Current theme
  locale: string; // Current locale
  authorized?: {
    // OAuth info (when guard is "oauth")
    sub?: string; // Subject identifier
    user_id?: string; // User ID
    team_id?: string; // Team ID (if team login)
    tenant_id?: string; // Tenant ID (multi-tenancy)
    client_id?: string; // OAuth client ID
    session_id?: string; // Session ID
    scope?: string; // OAuth scopes
    remember_me?: boolean; // Remember me flag

    // Data access constraints (set by ACL)
    constraints?: {
      owner_only?: boolean; // Only access owner's data
      creator_only?: boolean; // Only access creator's data
      editor_only?: boolean; // Only access editor's data
      team_only?: boolean; // Only access team's data
      extra?: Record<string, any>; // Custom constraints
    };
  };
}

Example Usage

function BeforeRender(request: Request): Record<string, any> {
  // Access query parameters
  const search = request.query.q;
  const page = parseInt(request.query.page) || 1;

  // Access route parameters
  const userId = request.params.id;

  // Access headers
  const authToken = request.headers["Authorization"];

  // Access session
  const sessionId = request.sid;

  return {
    search,
    page,
    userId,
  };
}

Process Calls

Use Process() to call Yao processes:

// Model operations
const users = Process("models.user.Get", { limit: 10 });
const user = Process("models.user.Find", userId);
Process("models.user.Save", userId, { name: "Updated" });
Process("models.user.Delete", userId);

// Custom scripts
const result = Process("scripts.utils.calculate", arg1, arg2);

// Session
const sessionUser = Process("session.Get", "user");
Process("session.Set", "key", "value");

// Flows
const output = Process("flows.myflow", input);

Error Handling

function ApiUpdateUser(id: string, data: any, request: Request): any {
  try {
    const user = Process("models.user.Find", id);
    if (!user) {
      throw new Error("User not found");
    }

    return Process("models.user.Save", id, data);
  } catch (error) {
    // Error will be returned to frontend
    throw new Error(`Failed to update user: ${error.message}`);
  }
}

Data Binding Methods (Called from .json)

In addition to Api prefixed methods (for frontend calls) and BeforeRender, you can define methods that are called directly from the page's .json configuration using the @MethodName syntax.

Naming Convention

Call Source Function Name Example Call
Frontend $Backend().Call() ApiMethodName $Backend().Call("MethodName")
.json data binding MethodName "$data": "@MethodName"
Before render BeforeRender Automatic

How It Works

When using @MethodName in .json, SUI calls the backend function with the Request object appended as the last argument:

// In .json: "$record": "@GetRecord"
// SUI internally calls: GetRecord(request)

function GetRecord(request: Request): any {
  // Access route parameters via request.params
  const id = request.params.id;
  return Process("models.record.Find", id);
}

With Additional Arguments

You can also pass arguments from .json:

{
  "$items": {
    "process": "@GetItems",
    "args": ["category_a", 10]
  }
}
// SUI calls: GetItems("category_a", 10, request)
// Arguments from .json come first, request is appended last

function GetItems(category: string, limit: number, request: Request): any[] {
  return Process("models.item.Get", {
    wheres: [{ column: "category", value: category }],
    limit: limit,
  });
}

Common Pitfall: Accessing Route Parameters

Wrong - $param is not available in backend scripts:

function GetRecord(): any {
  const id = $param.id; // ReferenceError: $param is not defined
  return Process("models.record.Find", id);
}

Correct - Use request.params:

function GetRecord(request: Request): any {
  const id = request.params.id; // Works!
  return Process("models.record.Find", id);
}

Complete Example

/users/profile/profile.backend.ts:

// Constants exported to frontend
const __sui_constants = {
  MAX_BIO_LENGTH: 500,
  ALLOWED_AVATAR_TYPES: ["image/jpeg", "image/png"],
};

// Helper functions exported to frontend
const __sui_helpers = ["formatDate", "truncate"];

function formatDate(date: string): string {
  return new Date(date).toLocaleDateString();
}

function truncate(text: string, length: number): string {
  if (text.length <= length) return text;
  return text.slice(0, length) + "...";
}

// Called before page render
function BeforeRender(request: Request): Record<string, any> {
  const userId = request.params.id;
  const user = Process("models.user.Find", userId);

  if (!user) {
    return { error: "User not found" };
  }

  const posts = Process("models.post.Get", {
    wheres: [{ column: "user_id", value: userId }],
    orders: [{ column: "created_at", option: "desc" }],
    limit: 10,
  });

  return {
    user,
    posts,
    isOwner: request.sid === user.session_id,
  };
}

// API: Get user posts
function ApiGetPosts(userId: string, page: number, request: Request): any {
  return Process("models.post.Paginate", {
    wheres: [{ column: "user_id", value: userId }],
    orders: [{ column: "created_at", option: "desc" }],
    page,
    pageSize: 10,
  });
}

// API: Update profile
function ApiUpdateProfile(data: any, request: Request): any {
  const sessionUser = Process("session.Get", "user");
  if (!sessionUser) {
    throw new Error("Not authenticated");
  }

  return Process("models.user.Save", sessionUser.id, {
    name: data.name,
    bio: data.bio?.slice(0, 500),
  });
}

// API: Upload avatar
function ApiUploadAvatar(file: any, request: Request): any {
  const sessionUser = Process("session.Get", "user");
  if (!sessionUser) {
    throw new Error("Not authenticated");
  }

  const result = Process("fs.system.Upload", file);
  Process("models.user.Save", sessionUser.id, {
    avatar: result.path,
  });

  return { avatar: result.path };
}