Plugins

Creating Custom Plugins

Learn how to create custom plugins for URPC with entities, adapters, and complete implementations.

Overview

Plugins in URPC allow you to extend functionality by defining custom entities and data source adapters. A plugin consists of:

  • Entities: Data structures that define your domain models
  • Adapters: Implementations that handle data operations for specific sources
  • Registration: Plugin configuration that connects entities with adapters

Plugin Structure

A typical plugin has the following structure:

my-plugin/
├── src/
│   ├── entities/
│   │   └── user.ts
│   ├── adapters/
│   │   └── user-adapter.ts
│   ├── plugins/
│   │   └── index.ts
│   └── index.ts
├── package.json
└── tsconfig.json

Step-by-Step Guide

Define Your Entity

Entities define the structure of your data. Use decorators from @unilab/urpc-core to specify field types:

// src/entities/user.ts
import { Fields } from "@unilab/urpc-core";

export class UserEntity {
  @Fields.string()
  id = "";

  @Fields.string()
  name = "";

  @Fields.string()
  email = "";

  @Fields.string()
  avatar = "";
}

Create Data Source Adapter

Adapters implement the BaseAdapter class to handle CRUD operations:

// src/adapters/user-adapter.ts
import {
  BaseAdapter,
  CreationArgs,
  FindManyArgs,
  FindOneArgs,
  URPCError,
  ErrorCodes,
} from "@unilab/urpc-core";
import { UserEntity } from "../entities/user";

// Mock data for demonstration
const userData = [
  {
    id: "1",
    name: "John Doe",
    email: "[email protected]",
    avatar: "https://example.com/avatar.png",
  },
  {
    id: "2",
    name: "Jane Smith",
    email: "[email protected]",
    avatar: "https://example.com/avatar2.png",
  },
];

export class UserAdapter extends BaseAdapter<UserEntity> {
  async findMany(args: FindManyArgs<UserEntity>): Promise<UserEntity[]> {
    const where = args?.where || {};
    
    if (where.id) {
      const idValue = typeof where.id === "object" ? where.id.$eq : where.id;
      return userData.filter((user) => user.id === idValue);
    }
    
    if (where.name) {
      const nameValue = typeof where.name === "object" ? where.name.$eq : where.name;
      return userData.filter((user) => user.name === nameValue);
    }
    
    if (where.email) {
      const emailValue = typeof where.email === "object" ? where.email.$eq : where.email;
      return userData.filter((user) => user.email === emailValue);
    }
    
    return userData;
  }

  async findOne(args: FindOneArgs<UserEntity>): Promise<UserEntity | null> {
    const { id } = args.where;
    if (!id) {
      return null;
    }
    return userData.find((user) => user.id === id) || null;
  }

  async create(args: CreationArgs<UserEntity>): Promise<UserEntity> {
    const { name, email, avatar } = args.data;
    if (!name || !email || !avatar) {
      throw new URPCError(ErrorCodes.BAD_REQUEST, "Invalid arguments");
    }
    
    const newUser = {
      id: (userData.length + 1).toString(),
      name,
      email,
      avatar,
    };
    userData.push(newUser);
    return newUser;
  }
}

Create the Plugin Definition

Define your plugin by combining entities and adapters:

// src/plugins/index.ts
import { Plugin } from "@unilab/urpc-core";
import { UserEntity } from "../entities/user";
import { UserAdapter } from "../adapters/user-adapter";

export const UserPlugin: Plugin = {
  entities: [UserEntity],
  adapters: [
    {
      source: "demo", 
      entity: "UserEntity",
      adapter: new UserAdapter()
    }
  ]
};

Export Plugin Components

Create the main entry point for your plugin:

// src/index.ts
export * from "./entities/user";
export * from "./adapters/user-adapter";
export * from "./plugins";

Using Your Plugin

Server Setup

Register your plugin in a URPC server:

// server.ts
import { URPC } from "@unilab/urpc-hono";
import { UserPlugin } from "./my-plugin";

const app = URPC.init({
  plugins: [UserPlugin]
});

export default {
  port: 3000,
  fetch: app.fetch
};

Client Usage

Use your plugin entities from the client:

// client.ts
import { repo, URPC } from "@unilab/urpc";
import { UserEntity } from "./my-plugin";

URPC.init({
  baseUrl: "http://localhost:3000",
  timeout: 10000
});

// Create a user
const createUser = async () => {
  const user = await repo<UserEntity>({
    entity: "user",
    source: "demo",
  }).create({
    data: {
      name: "Alice Johnson",
      email: "[email protected]",
      avatar: "https://example.com/avatar3.png"
    }
  });
  console.log("Created user:", user);
};

// Find users
const getUsers = async () => {
  const users = await repo<UserEntity>({
    entity: "user",
    source: "demo",
  }).findMany({
    where: { name: "John Doe" }
  });
  console.log("Users:", users);
};

// Update a user
const updateUser = async () => {
  const updated = await repo<UserEntity>({
    entity: "user",
    source: "demo",
  }).update({
    where: { id: "1" },
    data: { email: "[email protected]" }
  });
  console.log("Updated user:", updated);
};

createUser();
getUsers();
updateUser();

Package Configuration

Create a proper package.json for your plugin:

{
  "name": "@mycompany/urpc-user-plugin",
  "version": "1.0.0",
  "description": "User management plugin for URPC",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "scripts": {
    "build": "tsc",
    "test": "jest",
    "dev": "tsc --watch"
  },
  "dependencies": {
    "@unilab/urpc-core": "^1.0.0"
  },
  "devDependencies": {
    "typescript": "^5.0.0",
    "jest": "^29.0.0",
    "@types/jest": "^29.0.0"
  },
  "peerDependencies": {
    "@unilab/urpc-core": "^1.0.0"
  }
}

Next Steps