Skip to content

Instantly share code, notes, and snippets.

@tompere
Last active January 2, 2023 13:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tompere/1f543b4a9b7f134907e11b098b47372e to your computer and use it in GitHub Desktop.
Save tompere/1f543b4a9b7f134907e11b098b47372e to your computer and use it in GitHub Desktop.
a simple toolkit for creating openrpc-based nestjs controller

OpenRPC nest.js Toolkit

Overview

A simple toolkit for creating OpenRPC-compatible Nest.js controllers!

This toolkit provides two key features to help you build and deploy your server:

  • A build-time script that generates TypeScript declarations based on all OpenRPC documents in your codebase
  • A runtime decorator that allows you to make Nest.js controllers as OpenRPC-compatible.

Usage

Install

 npm i -S gist:1f543b4a9b7f134907e11b098b47372e

Add to build scripts

// package.json 
  "scripts": {
      "prebuild": "node ./node_modules/@tompere/openrpc-ts-generator/typings.js"
   }

Add decoartor to your controller:

import { JsonRpcEndpoint } from "@tompere/openrpc-ts-generator/JsonRpcEndpoint";
import * as openrpcDocument from './foo.openrpc.json';
import { FooRpcMethods } from './foo.openrpc'; // *.d.ts file

@JsonRpcEndpoint({
  openrpcDocument,
  version: 'v1',
  basePath: '/my-rpc-endpint', // optional, default: `/rpc`
})
export class FooController implements FooRpcMethods {
  // TODO implement according to interface definition
}
import { Controller, Post, HttpCode, HttpStatus, Body, UseGuards, CanActivate } from "@nestjs/common";
import { Router as RpcEndpoint } from "@open-rpc/server-js";
import type { JSONRPCRequest } from "@open-rpc/server-js/build/transports/server-transport";
import type { OpenrpcDocument } from "@open-rpc/meta-schema";
type Constructor = { new (...args: any[]): any };
type Config = { openrpcDocument: object; version: string; basePath?: string; authGuard?: CanActivate };
export function JsonRpcEndpoint({
openrpcDocument,
version,
basePath = "/rpc",
authGuard
}: Config) {
const guard: CanActivate = authGuard || { canActivate() { return true } }
return <C extends Constructor>(constructor: C) => {
@Controller(`${basePath}/${version}`)
class JsonRpcEndpointImpl extends constructor {
@Post("/")
@HttpCode(HttpStatus.OK)
@UseGuards(guard)
async rpc(@Body() { id, ...request }: JSONRPCRequest) {
const result = await this._callMethod(request);
return {
id,
jsonrpc: "2.0",
...result,
};
}
async _callMethod({ method, params }: JSONRPCRequest) {
const rpcEndpoint =
this._rpcEndpoint ||
(this._rpcEndpoint = new RpcEndpoint(
openrpcDocument as OpenrpcDocument,
this as any,
));
return rpcEndpoint.isMethodImplemented(method)
? await rpcEndpoint.call(method, params)
: RpcEndpoint.methodNotFoundHandler(method);
}
_rpcEndpoint: RpcEndpoint;
}
return JsonRpcEndpointImpl;
};
}
{
"name": "@tompere/openrpc-ts-generator",
"files": [
"typings.js",
"JsonRpcEndpoint.ts"
],
"dependencies": {
"@open-rpc/meta-schema": "^1.14.2",
"@open-rpc/server-js": "^1.9.3",
"@open-rpc/typings": "^1.12.1",
"change-case": "^4.1.2",
"globby": "^11.1.0"
}
}
const path = require('path');
const globby = require('globby');
const fs = require('fs');
const os = require('os');
const { promisify } = require('util');
const openrpcTypings = require('@open-rpc/typings');
const { pascalCase, camelCase } = require('change-case');
const { dereferenceDocument } = require('@open-rpc/schema-utils-js');
const { defaultResolver } = require('@json-schema-tools/reference-resolver');
const readfs = promisify(fs.readFile);
const writefs = promisify(fs.writeFile);
/**
*
* @param {*} declaration e.g. `export type Foo = (fp1: P) => Promise<Y>;`
* @returns e.g. `async foo(fp1: P): Promise<T>;`
*/
function convertTypeDeclaration(declaration) {
const [functionName, arrowFunctionDecleration] = declaration
.split('type')[1]
.trim()
.split(' = ');
const [params, returnType] = arrowFunctionDecleration.split('=>');
return `async ${camelCase(functionName)}${params.trim()}: ${returnType.trim()}`;}
function generateInterface(typings, name) {
const functions = typings
.getMethodTypings('typescript')
.split(os.EOL)
.map((line) => convertTypeDeclaration(line));
const symbol = [pascalCase(name.split('.')[0]), 'RpcMethods'].join('');
return `export interface ${symbol} {
${functions.join(os.EOL)}
}`;
}
async function exec() {
const openrpcDocPaths = await globby('src/**/*.openrpc.json', {});
const tasks = openrpcDocPaths.map(async (docPath) => {
const doc = await readfs(path.resolve(docPath));
const dereffedDocument = await dereferenceDocument(
JSON.parse(doc),
defaultResolver,
);
const typings = new openrpcTypings.default(dereffedDocument);
const { dir, name } = path.parse(docPath);
const interface = generateInterface(typings, name);
const dtsFilePath = path.resolve(dir, `${name}.d.ts`);
await writefs(
dtsFilePath,
[typings.toString('typescript'), interface].join(os.EOL),
);
return dtsFilePath;
});
return Promise.all(tasks);
}
exec().then((generatedFiles) =>
console.log(
['done generating the following file:', ...generatedFiles].join(os.EOL),
),
);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment