From a78818a21feb050b90e917360d6ccb4e1c8aaaeb Mon Sep 17 00:00:00 2001 From: Joshua Sosso Date: Thu, 18 Jul 2024 00:47:20 -0500 Subject: [PATCH 1/5] more standardized way of defining rpcs and routes --- languages/ts/ts-server/src/app.ts | 92 +++++++++-------------- languages/ts/ts-server/src/router.ts | 105 +++++++++++---------------- 2 files changed, 75 insertions(+), 122 deletions(-) diff --git a/languages/ts/ts-server/src/app.ts b/languages/ts/ts-server/src/app.ts index 364356ee..736e926d 100644 --- a/languages/ts/ts-server/src/app.ts +++ b/languages/ts/ts-server/src/app.ts @@ -21,7 +21,7 @@ import { type ArriServerError, defineError, handleH3Error } from "./errors"; import { isEventStreamRpc, registerEventStreamRpc } from "./eventStreamRpc"; import { type Middleware, type MiddlewareEvent } from "./middleware"; import { type ArriRoute, registerRoute } from "./route"; -import { ArriRouter, type ArriRouterBase } from "./router"; +import { ArriRouter, ArriService } from "./router"; import { createHttpRpcDefinition, getRpcParamName, @@ -30,12 +30,13 @@ import { isRpcParamSchema, type NamedRpc, registerRpc, - type RpcParamSchema, + Rpc, } from "./rpc"; import { createWsRpcDefinition, type NamedWebsocketRpc, registerWebsocketRpc, + WebsocketRpc, } from "./websocketRpc"; export type DefinitionMap = Record< @@ -45,7 +46,7 @@ export type DefinitionMap = Record< export const createAppDefinition = (def: AppDefinition) => def; -export class ArriApp implements ArriRouterBase { +export class ArriApp { __isArri__ = true; readonly h3App: App; readonly h3Router: Router = createRouter(); @@ -140,17 +141,17 @@ export class ArriApp implements ArriRouterBase { ); } - use(input: Middleware | ArriRouter): void { + use(input: Middleware | ArriRouter | ArriService): void { if (typeof input === "object" && input instanceof ArriRouter) { for (const route of input.getRoutes()) { this.route(route); } + this.registerDefinitions(input.getDefinitions()); + return; + } + if (typeof input === "object" && input instanceof ArriService) { for (const rpc of input.getProcedures()) { - if (rpc.transport === "http") { - this.rpc(rpc); - } else { - this.wsRpc(rpc); - } + this.rpc(rpc.name, rpc); } this.registerDefinitions(input.getDefinitions()); return; @@ -158,20 +159,15 @@ export class ArriApp implements ArriRouterBase { this._middlewares.push(input); } - rpc< - TIsEventStream extends boolean = false, - TParams extends AObjectSchema | undefined = undefined, - TResponse extends AObjectSchema | undefined = undefined, - >( - procedure: Omit< - NamedRpc, - "transport" - >, - ) { - (procedure as any).transport = "http"; - const p = procedure as NamedRpc; + rpc(name: string, procedure: Rpc | WebsocketRpc) { + (procedure as any).name = name; + const p = procedure as NamedRpc | NamedWebsocketRpc; const path = p.path ?? getRpcPath(p.name, this._rpcRoutePrefix); - this._procedures[p.name] = createHttpRpcDefinition(p.name, path, p); + if (p.transport === "http") { + this._procedures[p.name] = createHttpRpcDefinition(p.name, path, p); + } else if (p.transport === "ws") { + this._procedures[p.name] = createWsRpcDefinition(p.name, path, p); + } if (isRpcParamSchema(p.params)) { // eslint-disable-next-line @typescript-eslint/no-unsafe-argument @@ -187,8 +183,19 @@ export class ArriApp implements ArriRouterBase { this._definitions[responseName] = p.response; } } - if (isEventStreamRpc(p)) { - registerEventStreamRpc(this.h3Router, path, p, { + if (p.transport === "http") { + if (isEventStreamRpc(p)) { + registerEventStreamRpc(this.h3Router, path, p, { + middleware: this._middlewares, + onRequest: this._onRequest, + onError: this._onError, + onAfterResponse: this._onAfterResponse, + onBeforeResponse: this._onBeforeResponse, + debug: this._debug, + }); + return; + } + registerRpc(this.h3Router, path, p, { middleware: this._middlewares, onRequest: this._onRequest, onError: this._onError, @@ -198,42 +205,9 @@ export class ArriApp implements ArriRouterBase { }); return; } - registerRpc(this.h3Router, path, p, { - middleware: this._middlewares, - onRequest: this._onRequest, - onError: this._onError, - onAfterResponse: this._onAfterResponse, - onBeforeResponse: this._onBeforeResponse, - debug: this._debug, - }); - } - - wsRpc< - TParams extends RpcParamSchema | undefined, - TResponse extends RpcParamSchema | undefined, - >(procedure: Omit, "transport">) { - (procedure as any).transport = "ws"; - const p = procedure as NamedWebsocketRpc; - const path = - procedure.path ?? getRpcPath(procedure.name, this._rpcRoutePrefix); - this._procedures[procedure.name] = createWsRpcDefinition( - procedure.name, - path, - p, - ); - if (isRpcParamSchema(procedure.params)) { - const paramName = getRpcParamName(procedure.name, p); - if (paramName) { - this._definitions[paramName] = procedure.params; - } - } - if (isRpcParamSchema(procedure.response)) { - const responseName = getRpcResponseName(procedure.name, p); - if (responseName) { - this._definitions[responseName] = procedure.response; - } + if (p.transport === "ws") { + registerWebsocketRpc(this.h3Router, path, p); } - registerWebsocketRpc(this.h3Router, path, p); } route< diff --git a/languages/ts/ts-server/src/router.ts b/languages/ts/ts-server/src/router.ts index 453de007..d8251e06 100644 --- a/languages/ts/ts-server/src/router.ts +++ b/languages/ts/ts-server/src/router.ts @@ -1,70 +1,18 @@ -import { type AObjectSchema,type ASchema } from "@arrirpc/schema"; +import { type AObjectSchema, type ASchema } from "@arrirpc/schema"; import { type DefinitionMap } from "./app"; import { type ArriRoute } from "./route"; -import { type NamedRpc,type RpcParamSchema } from "./rpc"; -import { type NamedWebsocketRpc } from "./websocketRpc"; - -export interface ArriRouterBase { - rpc: < - TIsEventStream extends boolean, - TParams extends AObjectSchema | undefined, - TResponse extends AObjectSchema | undefined, - >( - procedure: Omit< - NamedRpc, - "transport" - >, - ) => void; - wsRpc: < - TParams extends RpcParamSchema | undefined, - TResponse extends RpcParamSchema | undefined, - >( - procedure: Omit, "transport">, - ) => void; - route: < - TPath extends string, - TQuery extends AObjectSchema = any, - TBody extends ASchema = any, - TResponse = any, - >( - route: ArriRoute, - ) => void; - - registerDefinitions: (definitions: DefinitionMap) => void; -} - -export class ArriRouter implements ArriRouterBase { - private readonly procedures: Array< - NamedRpc | NamedWebsocketRpc - > = []; +import { type NamedRpc, Rpc } from "./rpc"; +import { type NamedWebsocketRpc, WebsocketRpc } from "./websocketRpc"; +export class ArriRouter { private readonly routes: Array> = []; - private readonly definitions: DefinitionMap = {}; - rpc< - TIsEventStream extends boolean = false, - TParams extends AObjectSchema | undefined = undefined, - TResponse extends AObjectSchema | undefined = undefined, - >( - procedure: Omit< - NamedRpc, - "transport" - >, - ) { - (procedure as any).transport = "http"; - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - this.procedures.push(procedure as any); - } + prefix: string; - wsRpc< - TParams extends RpcParamSchema | undefined, - TResponse extends RpcParamSchema | undefined, - >(procedure: Omit, "transport">) { - (procedure as any).transport = "ws"; - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - this.procedures.push(procedure as any); + constructor(routePrefix = "") { + this.prefix = routePrefix; } route< @@ -73,6 +21,7 @@ export class ArriRouter implements ArriRouterBase { TBody extends ASchema = any, TResponse = any, >(route: ArriRoute) { + route.path = `${this.prefix}${route.path}` as any; this.routes.push(route); } @@ -82,10 +31,6 @@ export class ArriRouter implements ArriRouterBase { } } - getProcedures() { - return this.procedures; - } - getRoutes() { return this.routes; } @@ -94,3 +39,37 @@ export class ArriRouter implements ArriRouterBase { return this.definitions; } } + +export class ArriService { + name: string; + + constructor(name: string) { + this.name = name; + } + + private readonly procedures: ( + | NamedRpc + | NamedWebsocketRpc + )[] = []; + + private readonly definitions: DefinitionMap = {}; + + rpc(name: string, procedure: Rpc | WebsocketRpc) { + (procedure as any).name = `${this.name}.${name}`; + this.procedures.push(procedure as any); + } + + registerDefinitions(models: DefinitionMap) { + for (const key of Object.keys(models)) { + this.definitions[key] = models[key]!; + } + } + + getProcedures() { + return this.procedures; + } + + getDefinitions() { + return this.definitions; + } +} From dc5a432aac126c0a6a3f8535926e802be313a44a Mon Sep 17 00:00:00 2001 From: joshmossas Date: Thu, 18 Jul 2024 23:08:25 -0500 Subject: [PATCH 2/5] refactor how services and rpcs are defined --- languages/ts/ts-server/src/_index.ts | 1 + languages/ts/ts-server/src/app.test.ts | 53 +++++++++++--------- languages/ts/ts-server/src/app.ts | 29 ++++++++--- languages/ts/ts-server/src/eventStreamRpc.ts | 4 +- languages/ts/ts-server/src/router.ts | 36 ------------- languages/ts/ts-server/src/rpc.ts | 39 ++++++++------ languages/ts/ts-server/src/service.ts | 48 ++++++++++++++++++ playground/src/app.ts | 34 ++++++++++++- tooling/cli/src/common.ts | 28 ++--------- 9 files changed, 162 insertions(+), 110 deletions(-) create mode 100644 languages/ts/ts-server/src/service.ts diff --git a/languages/ts/ts-server/src/_index.ts b/languages/ts/ts-server/src/_index.ts index 8fd5b366..4f988beb 100644 --- a/languages/ts/ts-server/src/_index.ts +++ b/languages/ts/ts-server/src/_index.ts @@ -6,5 +6,6 @@ export * from "./middleware"; export * from "./route"; export * from "./router"; export * from "./rpc"; +export * from "./service"; export * from "./websocketRpc"; export * from "h3"; diff --git a/languages/ts/ts-server/src/app.test.ts b/languages/ts/ts-server/src/app.test.ts index 911e1543..86087a1e 100644 --- a/languages/ts/ts-server/src/app.test.ts +++ b/languages/ts/ts-server/src/app.test.ts @@ -2,6 +2,8 @@ import { type AppDefinition } from "@arrirpc/codegen-utils"; import { a } from "@arrirpc/schema"; import { ArriApp } from "./app"; +import { defineEventStreamRpc } from "./eventStreamRpc"; +import { defineRpc } from "./rpc"; it("creates valid app definition", () => { const app = new ArriApp(); @@ -17,30 +19,33 @@ it("creates valid app definition", () => { }, { id: "SayHelloResponse" }, ); - app.rpc({ - name: "sayHello", - params: SayHelloParams, - response: SayHelloResponse, - handler({ params }) { - return { - message: `Hello ${params.name}`, - }; - }, - }); - app.rpc({ - name: "sayHelloStream", - params: SayHelloParams, - response: SayHelloResponse, - isEventStream: true, - handler({ params, stream }) { - const timeout = setInterval(async () => { - await stream.push({ message: `Hello ${params.name}` }); - }, 100); - stream.onClosed(() => { - clearInterval(timeout); - }); - }, - }); + app.rpc( + "sayHello", + defineRpc({ + params: SayHelloParams, + response: SayHelloResponse, + handler({ params }) { + return { + message: `Hello ${params.name}`, + }; + }, + }), + ); + app.rpc( + "sayHelloStream", + defineEventStreamRpc({ + params: SayHelloParams, + response: SayHelloResponse, + handler({ params, stream }) { + const timeout = setInterval(async () => { + await stream.push({ message: `Hello ${params.name}` }); + }, 100); + stream.onClosed(() => { + clearInterval(timeout); + }); + }, + }), + ); const def = app.getAppDefinition(); const expectedResult: AppDefinition = { diff --git a/languages/ts/ts-server/src/app.ts b/languages/ts/ts-server/src/app.ts index 736e926d..07bd5765 100644 --- a/languages/ts/ts-server/src/app.ts +++ b/languages/ts/ts-server/src/app.ts @@ -14,6 +14,7 @@ import { createRouter, eventHandler, type Router, + setResponseHeader, setResponseStatus, } from "h3"; @@ -21,22 +22,22 @@ import { type ArriServerError, defineError, handleH3Error } from "./errors"; import { isEventStreamRpc, registerEventStreamRpc } from "./eventStreamRpc"; import { type Middleware, type MiddlewareEvent } from "./middleware"; import { type ArriRoute, registerRoute } from "./route"; -import { ArriRouter, ArriService } from "./router"; +import { ArriRouter } from "./router"; import { createHttpRpcDefinition, getRpcParamName, getRpcPath, getRpcResponseName, isRpcParamSchema, - type NamedRpc, + type NamedHttpRpc, registerRpc, Rpc, } from "./rpc"; +import { ArriService } from "./service"; import { createWsRpcDefinition, type NamedWebsocketRpc, registerWebsocketRpc, - WebsocketRpc, } from "./websocketRpc"; export type DefinitionMap = Record< @@ -84,7 +85,14 @@ export class ArriApp { if (!opts.disableDefinitionRoute) { this.h3Router.get( this.definitionPath, - eventHandler(() => this.getAppDefinition()), + eventHandler((event) => { + setResponseHeader( + event, + "Content-Type", + "application/json", + ); + return this.getAppDefinition(); + }), ); } if (!opts.disableDefaultRoute) { @@ -117,7 +125,14 @@ export class ArriApp { if (process.env.ARRI_DEV_MODE === "true") { this.h3Router.get( DEV_DEFINITION_ENDPOINT, - eventHandler(() => this.getAppDefinition()), + eventHandler((event) => { + setResponseHeader( + event, + "Content-Type", + "application/json", + ); + return this.getAppDefinition(); + }), ); } // default fallback route @@ -159,9 +174,9 @@ export class ArriApp { this._middlewares.push(input); } - rpc(name: string, procedure: Rpc | WebsocketRpc) { + rpc(name: string, procedure: Rpc) { (procedure as any).name = name; - const p = procedure as NamedRpc | NamedWebsocketRpc; + const p = procedure as NamedHttpRpc | NamedWebsocketRpc; const path = p.path ?? getRpcPath(p.name, this._rpcRoutePrefix); if (p.transport === "http") { this._procedures[p.name] = createHttpRpcDefinition(p.name, path, p); diff --git a/languages/ts/ts-server/src/eventStreamRpc.ts b/languages/ts/ts-server/src/eventStreamRpc.ts index 9e78f854..04e1ab78 100644 --- a/languages/ts/ts-server/src/eventStreamRpc.ts +++ b/languages/ts/ts-server/src/eventStreamRpc.ts @@ -15,9 +15,9 @@ import { handleH3Error } from "./errors"; import { type MiddlewareEvent } from "./middleware"; import { type RouteOptions } from "./route"; import { + type HttpRpc, isRpc, isRpcParamSchema, - type Rpc, type RpcEvent, type RpcParamSchema, validateRpcRequestInput, @@ -51,7 +51,7 @@ export function isEventStreamRpc( export interface EventStreamRpc< TParams extends RpcParamSchema | undefined = undefined, TResponse extends RpcParamSchema | undefined = undefined, -> extends Omit, "handler" | "postHandler"> { +> extends Omit, "handler" | "postHandler"> { isEventStream: true; handler: EventStreamRpcHandler< TParams extends RpcParamSchema ? InferType : undefined, diff --git a/languages/ts/ts-server/src/router.ts b/languages/ts/ts-server/src/router.ts index d8251e06..a65e3301 100644 --- a/languages/ts/ts-server/src/router.ts +++ b/languages/ts/ts-server/src/router.ts @@ -2,8 +2,6 @@ import { type AObjectSchema, type ASchema } from "@arrirpc/schema"; import { type DefinitionMap } from "./app"; import { type ArriRoute } from "./route"; -import { type NamedRpc, Rpc } from "./rpc"; -import { type NamedWebsocketRpc, WebsocketRpc } from "./websocketRpc"; export class ArriRouter { private readonly routes: Array> = []; @@ -39,37 +37,3 @@ export class ArriRouter { return this.definitions; } } - -export class ArriService { - name: string; - - constructor(name: string) { - this.name = name; - } - - private readonly procedures: ( - | NamedRpc - | NamedWebsocketRpc - )[] = []; - - private readonly definitions: DefinitionMap = {}; - - rpc(name: string, procedure: Rpc | WebsocketRpc) { - (procedure as any).name = `${this.name}.${name}`; - this.procedures.push(procedure as any); - } - - registerDefinitions(models: DefinitionMap) { - for (const key of Object.keys(models)) { - this.definitions[key] = models[key]!; - } - } - - getProcedures() { - return this.procedures; - } - - getDefinitions() { - return this.definitions; - } -} diff --git a/languages/ts/ts-server/src/rpc.ts b/languages/ts/ts-server/src/rpc.ts index 249139eb..a0883503 100644 --- a/languages/ts/ts-server/src/rpc.ts +++ b/languages/ts/ts-server/src/rpc.ts @@ -26,7 +26,7 @@ import { } from "h3"; import { kebabCase, pascalCase } from "scule"; -import { type RpcEventContext,type RpcPostEventContext } from "./context"; +import { type RpcEventContext, type RpcPostEventContext } from "./context"; import { defineError, handleH3Error } from "./errors"; import { type EventStreamRpc, @@ -44,15 +44,23 @@ export function isRpcParamSchema(input: unknown): input is RpcParamSchema { return isAObjectSchema(input) || isADiscriminatorSchema(input); } -export interface NamedRpc< +export interface NamedHttpRpc< TIsEventStream extends boolean = false, TParams extends RpcParamSchema | undefined = undefined, TResponse extends RpcParamSchema | undefined = undefined, -> extends Rpc { +> extends HttpRpc { name: string; } -export interface Rpc< +export type Rpc< + TIsEventStream extends boolean = false, + TParams extends RpcParamSchema | undefined = undefined, + TResponse extends RpcParamSchema | undefined = undefined, +> = + | HttpRpc + | WebsocketRpc; + +export interface HttpRpc< TIsEventStream extends boolean = false, TParams extends RpcParamSchema | undefined = undefined, TResponse extends RpcParamSchema | undefined = undefined, @@ -87,10 +95,6 @@ export interface Rpc< : undefined >; } -export type HttpRpc< - TParams extends RpcParamSchema | undefined, - TResponse extends RpcParamSchema | undefined, -> = Omit, "isEventStream">; export interface RpcEvent extends Omit { @@ -112,7 +116,7 @@ export type RpcPostHandler = ( event: RpcPostEvent, ) => any; -export function isRpc(input: unknown): input is Rpc { +export function isRpc(input: unknown): input is HttpRpc { return ( typeof input === "object" && input !== null && @@ -127,8 +131,11 @@ export function defineRpc< TParams extends RpcParamSchema | undefined = undefined, TResponse extends RpcParamSchema | undefined | never = undefined, >( - config: Omit, "transport">, -): Rpc { + config: Omit< + HttpRpc, + "transport" | "isEventStream" + >, +): HttpRpc { (config as any).transport = "http"; return config as any; } @@ -136,7 +143,7 @@ export function defineRpc< export function createHttpRpcDefinition( rpcName: string, httpPath: string, - procedure: Rpc, + procedure: HttpRpc, ): HttpRpcDefinition { let method: RpcHttpMethod; if (procedure.isEventStream === true) { @@ -172,7 +179,7 @@ export function getRpcPath(rpcName: string, prefix = ""): string { export function getRpcParamName( rpcName: string, - procedure: Rpc | WebsocketRpc, + procedure: HttpRpc | WebsocketRpc, ): string | undefined { if (!isRpcParamSchema(procedure.params)) { return undefined; @@ -190,7 +197,7 @@ export function getRpcParamName( export function getRpcResponseName( rpcName: string, - procedure: Rpc | WebsocketRpc, + procedure: HttpRpc | WebsocketRpc, ): string | undefined { if (!isRpcParamSchema(procedure.response)) { return undefined; @@ -209,7 +216,7 @@ export function getRpcResponseName( function getRpcResponseDefinition( rpcName: string, procedure: - | Rpc + | HttpRpc | EventStreamRpc | WebsocketRpc, ): RpcDefinition["response"] { @@ -226,7 +233,7 @@ function getRpcResponseDefinition( export function registerRpc( router: Router, path: string, - procedure: NamedRpc, + procedure: NamedHttpRpc, opts: RouteOptions, ) { let responseValidator: undefined | ReturnType; diff --git a/languages/ts/ts-server/src/service.ts b/languages/ts/ts-server/src/service.ts new file mode 100644 index 00000000..1b3399e4 --- /dev/null +++ b/languages/ts/ts-server/src/service.ts @@ -0,0 +1,48 @@ +import { DefinitionMap } from "./app"; +import { HttpRpc, NamedHttpRpc, Rpc } from "./rpc"; +import { NamedWebsocketRpc, WebsocketRpc } from "./websocketRpc"; + +export class ArriService { + name: string; + + constructor(name: string) { + this.name = name; + } + + private readonly procedures: ( + | NamedHttpRpc + | NamedWebsocketRpc + )[] = []; + + private readonly definitions: DefinitionMap = {}; + + rpc(name: string, procedure: Rpc) { + (procedure as any).name = `${this.name}.${name}`; + this.procedures.push(procedure as any); + } + + registerDefinitions(models: DefinitionMap) { + for (const key of Object.keys(models)) { + this.definitions[key] = models[key]!; + } + } + + getProcedures() { + return this.procedures; + } + + getDefinitions() { + return this.definitions; + } +} + +export function defineService( + name: string, + procedures: Record | WebsocketRpc>, +): ArriService { + const service = new ArriService(name); + for (const key of Object.keys(procedures)) { + service.rpc(key, procedures[key]!); + } + return service; +} diff --git a/playground/src/app.ts b/playground/src/app.ts index 083faa68..48c3223f 100644 --- a/playground/src/app.ts +++ b/playground/src/app.ts @@ -1,4 +1,5 @@ -import { ArriApp, handleCors } from "@arrirpc/server"; +import { a } from "@arrirpc/schema"; +import { ArriApp, defineRpc, defineService, handleCors } from "@arrirpc/server"; const app = new ArriApp({ async onRequest(event) { @@ -6,6 +7,37 @@ const app = new ArriApp({ origin: "*", }); }, + onError(error) { + console.log({ + name: error.name, + code: error.code, + message: error.message, + cause: error.cause, + stack: error.stack, + }); + }, }); +const usersService = defineService("users", { + getUser: defineRpc({ + params: a.object("GetUserParams", { + userId: a.string(), + }), + response: a.object("User", { + id: a.string(), + name: a.string(), + createdAt: a.timestamp(), + }), + handler({ params }) { + return { + id: params.userId, + name: "", + createdAt: new Date(), + }; + }, + }), +}); + +app.use(usersService); + export default app; diff --git a/tooling/cli/src/common.ts b/tooling/cli/src/common.ts index 8006f558..f9eedd82 100644 --- a/tooling/cli/src/common.ts +++ b/tooling/cli/src/common.ts @@ -77,30 +77,10 @@ export async function createAppWithRoutesModule(config: ResolvedArriConfig) { `import ${route.importName} from '${route.importPath}';`, ) .join("\n")} - const routes: {id: string; route: any}[] = [${routes - .map( - (route) => - `{ id: '${route.name}', route: ${route.importName} }`, - ) - .join(",\n")} - ]; - for(const route of routes) { - if(route.route.transport === 'http') { - app.rpc({ - name: route.id, - ...route.route, - }); - continue; - } - if (route.route.transport === 'ws') { - app.wsRpc({ - name: route.id, - ...route.route, - }); - continue; - } - } - export default app`, + + ${routes.map((route) => `app.rpc('${route.name}', ${route.importName});`)} + + export default app;`, { parser: "typescript", tabWidth: 4 }, ); await fs.writeFile( From d063804076743a626447c94c5147fc00c8a7603c Mon Sep 17 00:00:00 2001 From: joshmossas Date: Fri, 19 Jul 2024 00:20:04 -0500 Subject: [PATCH 3/5] clean up type inference --- languages/ts/ts-server/README.md | 37 ++++++++--- languages/ts/ts-server/src/app.ts | 16 +++-- languages/ts/ts-server/src/context.ts | 43 ++++++------ languages/ts/ts-server/src/errors.ts | 2 +- languages/ts/ts-server/src/route.ts | 4 +- tests/server/src/app.ts | 1 + tooling/codegen-utils/README.md | 94 +++++++++++++++++++++++++-- 7 files changed, 153 insertions(+), 44 deletions(-) diff --git a/languages/ts/ts-server/README.md b/languages/ts/ts-server/README.md index fd2e7c0e..da21d5d9 100644 --- a/languages/ts/ts-server/README.md +++ b/languages/ts/ts-server/README.md @@ -189,13 +189,17 @@ For those that want to opt out of the file-based routing system you can manually ```ts // using the app instance const app = new ArriApp() -app.rpc('sayHello', {...}) +app.rpc('sayHello', + defineRpc({...}) +); -// using a sub-router +// defining a service const app = new ArriApp(); -const router = new ArriRouter(); -router.rpc('sayHello', {...}) -app.use(router) +const usersService = defineService("users", { + getUser: defineRpc({..}), + createUser: defineRpc({..}), +}); +app.use(usersService); ``` #### Creating Event Stream Procedures @@ -268,11 +272,14 @@ export default defineEventStreamRpc({ #### EventStreamConnection methods ```ts -stream.push(data: Data, eventId?: string) -stream.pushError(error: ArriRequestError, eventId?: string) +// send the stream to the client. Must be called before pushing any messages stream.send() -stream.end() -stream.on(e: 'request:close' | 'close', callback: () => any) +// push a new message to the client +stream.push(data: Data, eventId?: string) +// close the stream and tell the client that there will be no more messages +stream.close() +// register a callback that will fire after the stream has been close by the server or the connection has been dropped +stream.onClosed(cb: () => any) ``` ### Creating Websocket Procedures (Experimental) @@ -363,6 +370,16 @@ router.route({ } }) app.use(router) + +// sup-routers can also specify a route prefix +const router = new ArriRouter("/v1") +router.route({ + method: "get", + path: "/hello-world", // this will become /v1/hello-world + handler(event) { + return "hello world" + } +}); ``` ### Adding Middleware @@ -440,7 +457,7 @@ export default defineConfig({ For info on what generators are available see [here](/README.md#client-generators) -For info on how to create your own generator see [] +For info on how to create your own generator see [@arrirpc/codegen-utils](/tooling/codegen-utils/README.md) ## Key Concepts diff --git a/languages/ts/ts-server/src/app.ts b/languages/ts/ts-server/src/app.ts index 07bd5765..f0fff3f1 100644 --- a/languages/ts/ts-server/src/app.ts +++ b/languages/ts/ts-server/src/app.ts @@ -13,14 +13,16 @@ import { createApp, createRouter, eventHandler, + H3Event, type Router, setResponseHeader, setResponseStatus, } from "h3"; +import { RequestHookContext } from "./context"; import { type ArriServerError, defineError, handleH3Error } from "./errors"; import { isEventStreamRpc, registerEventStreamRpc } from "./eventStreamRpc"; -import { type Middleware, type MiddlewareEvent } from "./middleware"; +import { type Middleware, MiddlewareEvent } from "./middleware"; import { type ArriRoute, registerRoute } from "./route"; import { ArriRouter } from "./router"; import { @@ -99,7 +101,7 @@ export class ArriApp { this.route({ method: ["get", "head"], path: "/", - handler: (_) => { + handler: async (_) => { const response: Record = { title: this.appInfo?.title ?? "Arri-RPC Server", description: @@ -277,10 +279,14 @@ export interface ArriOptions { disableDefaultRoute?: boolean; disableDefinitionRoute?: boolean; onRequest?: (event: MiddlewareEvent) => void | Promise; - onAfterResponse?: (event: MiddlewareEvent) => void | Promise; - onBeforeResponse?: (event: MiddlewareEvent) => void | Promise; + onAfterResponse?: (event: RequestHookEvent) => void | Promise; + onBeforeResponse?: (event: RequestHookEvent) => void | Promise; onError?: ( error: ArriServerError, - event: MiddlewareEvent, + event: RequestHookContext, ) => void | Promise; } + +export interface RequestHookEvent extends Omit { + context: RequestHookContext; +} diff --git a/languages/ts/ts-server/src/context.ts b/languages/ts/ts-server/src/context.ts index 49e53821..2aa2f77a 100644 --- a/languages/ts/ts-server/src/context.ts +++ b/languages/ts/ts-server/src/context.ts @@ -1,43 +1,44 @@ -import { type H3EventContext } from "h3"; - import { type ExtractParams } from "./middleware"; -export type ArriEventContext = Record; +export interface ArriEventContext extends Record {} -export interface RpcEventContext - extends ArriEventContext, - Omit { +export type RpcEventContext = ArriEventContext & { rpcName: string; params: TParams; -} +}; -export interface RpcPostEventContext - extends RpcEventContext { +export type RpcPostEventContext< + TParams = undefined, + TResponse = undefined, +> = RpcEventContext & { response: TResponse; -} +}; -export interface RouteEventContext< +export type RouteEventContext< TPath extends string, TQuery extends Record = any, TBody = any, -> extends ArriEventContext, - H3EventContext { +> = ArriEventContext & { params: ExtractParams; query: TQuery; body: TBody; -} +}; -export interface RoutePostEventContext< +export type RoutePostEventContext< TPath extends string, TQuery extends Record = any, TBody = any, TResponse = any, -> extends RouteEventContext { +> = RouteEventContext & { response: TResponse; -} +}; + +export type MiddlewareEventContext = ArriEventContext & { + rpcName?: string; +}; -export interface MiddlewareEventContext - extends ArriEventContext, - H3EventContext { +export type RequestHookContext = ArriEventContext & { rpcName?: string; -} + params?: Record; + response?: unknown; +}; diff --git a/languages/ts/ts-server/src/errors.ts b/languages/ts/ts-server/src/errors.ts index ba51bafc..91985d21 100644 --- a/languages/ts/ts-server/src/errors.ts +++ b/languages/ts/ts-server/src/errors.ts @@ -267,7 +267,7 @@ export async function handleH3Error( } setResponseStatus(event, arriErr.code); if (onError) { - await onError(arriErr, event); + await onError(arriErr, event as any); } if (event.handled) { return; diff --git a/languages/ts/ts-server/src/route.ts b/languages/ts/ts-server/src/route.ts index 8b19d10f..c9a35443 100644 --- a/languages/ts/ts-server/src/route.ts +++ b/languages/ts/ts-server/src/route.ts @@ -19,9 +19,9 @@ import { } from "h3"; import { type ArriOptions } from "./app"; -import { type RouteEventContext,type RoutePostEventContext } from "./context"; +import { type RouteEventContext, type RoutePostEventContext } from "./context"; import { defineError, handleH3Error } from "./errors"; -import { type Middleware,type MiddlewareEvent } from "./middleware"; +import { type Middleware, type MiddlewareEvent } from "./middleware"; export interface RouteEvent< TPath extends string, diff --git a/tests/server/src/app.ts b/tests/server/src/app.ts index cf992469..204fa3d8 100644 --- a/tests/server/src/app.ts +++ b/tests/server/src/app.ts @@ -15,6 +15,7 @@ const app = new ArriApp({ version: "10", }, onRequest(event) { + event.context; handleCors(event, { origin: "*", }); diff --git a/tooling/codegen-utils/README.md b/tooling/codegen-utils/README.md index 31975966..0129eb5f 100644 --- a/tooling/codegen-utils/README.md +++ b/tooling/codegen-utils/README.md @@ -1,11 +1,95 @@ # @arrirpc/codegen-utils -This library was generated with [Nx](https://nx.dev). +This library contains a number of utilities that to assist in creating generators for Arri RPC. To See more complete usage example checkout one of the official arri client generators. -## Building +## Creating a Generator Plugin -Run `nx build @arrirpc/codegen-utils` to build the library. +```ts +import { defineGeneratorPlugin } from "@arrirpc/codegen-utils"; -## Running unit tests +// add any options needed for your plugin here +export interface MyPluginOptions { + a: string; + b: string; +} -Run `nx test @arrirpc/codegen-utils` to execute the unit tests via [Vitest](https://vitest.dev). +export default defineGeneratorPlugin((options: MyPluginOptions) => { + return { + options, + generator: async (appDef, isDevServer) => { + // generate something using the app definition and the specified options + }, + }; +}); +``` + +## Other Utilities + +```ts +// type guards +isAddDefinition(input); +isRpcDefinition(input); +isServiceDefinition(input); +isSchema(input); +isSchemaFormEmpty(input); +isSchemaFormType(input); +isSchemaFormEnum(input); +isSchemaFormElements(inputs); +isSchemaFormProperties(input); +isSchemaFormValues(input); +isSchemaFormDiscriminator(input); +isSchemaFormRef(input); + +unflattenProcedures({ + "v1.users.getUser": { + transport: "http", + path: "/v1/users/get-user", + method: "get", + }, + "v1.users.createUser": { + transport: "http", + path: "/v1/users/create-user", + method: "post", + }, +}); +/** + * outputs the following + * { + * v1: { + * users: { + * getUser: { + * transport: "http", + * path: "/v1/users/get-user", + * method: "get", + * }, + * createUser: { + * transport: "http", + * path: "/v1/users/create-user", + * method: "post", + * } + * } + * } + * } + */ + +removeDisallowedChars(input, disallowedChars); +camelCase(input, opts); +kebabCase(input); +pascalCase(input, opts); +snakeCase(input, opts); +titleCase(input, opts); +flatCase(input, opts); +upperFirst(input); +lowerFirst(input); +isUppercase(input); +``` + +## Development + +### Building + +Run `pnpm nx build codegen-utils` to build the library. + +### Running unit tests + +Run `pnpm nx test codegen-utils` to execute the unit tests via [Vitest](https://vitest.dev). From 64e41c45a438c82c712faf82aa8dc6413e16ef24 Mon Sep 17 00:00:00 2001 From: joshmossas Date: Fri, 19 Jul 2024 00:51:32 -0500 Subject: [PATCH 4/5] fix type errors + build issues --- .github/workflows/tests.yaml | 1 + README.md | 13 ++-- languages/ts/ts-server/src/service.ts | 9 ++- tests/server/src/app.ts | 3 +- .../src/procedures/users/watchUser.rpc.ts | 12 +-- tests/server/src/routes/other.ts | 76 +++++++++---------- tooling/cli/src/common.ts | 2 +- 7 files changed, 58 insertions(+), 58 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 4256d297..5a93a5ac 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -34,6 +34,7 @@ jobs: - run: pnpm nx affected -t pub -- get # install dart dependencies for affected projects - run: pnpm run build - run: pnpm nx affected -t lint + - run: pnpm nx affected -t typecheck unit-tests: if: github.event.pull_request.draft == false runs-on: ubuntu-latest diff --git a/README.md b/README.md index 8a3e62f3..597ee0cd 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,15 @@ Arri is an RPC framework designed for effortless end-to-end type safety across p This is a work in progress. Things will break. -## Schema Builder +## Official Server Implementations -[@arrirpc/schema](tooling/schema/README.md) is used to define types that can be generated in any language. It also doubles as a parsing and serialization library that can be used on a NodeJS backend. +- [Typescript](/languages/ts/ts-server/README.md) -## Server Implementations +When I have time I would like to add more languages to this list. Currently I have the following lanaguages on my shortlist for potential server implementations: -- [Typescript](languages/ts/ts-server/README.md) - Official ts server implementation. It uses [@arrirpc/schema](tooling/schma/README.md) to define language agnostic types and safely parse/serialize inputs and outputs. +- go +- rust +- dart ## Client Generators @@ -32,7 +34,8 @@ Below are the language client generators that are planned to have first party su ## Other Tooling -- [arri CLI](/tooling/cli/README.md) - CLI tool for run code generators and managing dependencies +- [Arri CLI](/tooling/cli/README.md) - CLI tool for run code generators and managing dependencies +- [@arrirpc/schema](tooling/schema/README.md) - Arri type builder used to define types that can be generated in multiple languages. It also doubles as a parsing and serialization library that can be used on a NodeJS backend. - [@arrirpc/typebox-adapter](tooling/schema-typebox-adapter/README.md) - convert Typebox Schemas to Arri Type Definitions - [@arrirpc/eslint-plugin](tooling/eslint-plugin/README.md) - Useful eslint rules when making Arri Type Definitions diff --git a/languages/ts/ts-server/src/service.ts b/languages/ts/ts-server/src/service.ts index 1b3399e4..6191d59d 100644 --- a/languages/ts/ts-server/src/service.ts +++ b/languages/ts/ts-server/src/service.ts @@ -38,11 +38,14 @@ export class ArriService { export function defineService( name: string, - procedures: Record | WebsocketRpc>, + procedures?: Record< + string, + HttpRpc | WebsocketRpc + >, ): ArriService { const service = new ArriService(name); - for (const key of Object.keys(procedures)) { - service.rpc(key, procedures[key]!); + for (const key of Object.keys(procedures ?? {})) { + service.rpc(key, procedures![key]!); } return service; } diff --git a/tests/server/src/app.ts b/tests/server/src/app.ts index 204fa3d8..9a7d824b 100644 --- a/tests/server/src/app.ts +++ b/tests/server/src/app.ts @@ -7,7 +7,7 @@ import { handleCors, } from "@arrirpc/server"; -import manualRouter from "./routes/other"; +import { manualRouter, manualService } from "./routes/other"; const app = new ArriApp({ rpcRoutePrefix: "rpcs", @@ -53,5 +53,6 @@ app.registerDefinitions({ }); app.use(manualRouter); +app.use(manualService); export default app; diff --git a/tests/server/src/procedures/users/watchUser.rpc.ts b/tests/server/src/procedures/users/watchUser.rpc.ts index ab060cb3..d7826be4 100644 --- a/tests/server/src/procedures/users/watchUser.rpc.ts +++ b/tests/server/src/procedures/users/watchUser.rpc.ts @@ -58,8 +58,8 @@ export default defineEventStreamRpc({ userId: a.string(), }), response: TestUser, - async handler({ params, connection }) { - connection.start(); + async handler({ params, stream }) { + stream.send(); const user: TestUser = { id: params.userId, role: "standard", @@ -76,17 +76,17 @@ export default defineEventStreamRpc({ metadata: {}, randomList: [], }; - await connection.push(user, randomUUID()); + await stream.push(user, randomUUID()); let count = 1; const interval = setInterval(async () => { - await connection.push(user, randomUUID()); + await stream.push(user, randomUUID()); count++; if (count >= 10) { - await connection.end(); + await stream.close(); } }, 500); - connection.on("disconnect", () => { + stream.onClosed(() => { clearInterval(interval); }); }, diff --git a/tests/server/src/routes/other.ts b/tests/server/src/routes/other.ts index a91e0f46..cf022405 100644 --- a/tests/server/src/routes/other.ts +++ b/tests/server/src/routes/other.ts @@ -1,13 +1,12 @@ import { a } from "@arrirpc/schema"; -import { ArriRouter } from "@arrirpc/server"; - -const router = new ArriRouter(); +import { ArriRouter, defineRpc, defineService } from "@arrirpc/server"; +export const manualRouter = new ArriRouter(); const DefaultPayload = a.object("DefaultPayload", { message: a.string(), }); -router.route({ +manualRouter.route({ path: "/routes/hello-world", method: ["get", "post"], handler(_) { @@ -15,42 +14,35 @@ router.route({ }, }); -router.rpc({ - method: "get", - name: "tests.emptyParamsGetRequest", - params: undefined, - response: DefaultPayload, - handler() { - return { - message: "ok", - }; - }, -}); - -router.rpc({ - name: "tests.emptyParamsPostRequest", - params: undefined, - response: DefaultPayload, - handler() { - return { - message: "ok", - }; - }, -}); - -router.rpc({ - method: "get", - name: "tests.emptyResponseGetRequest", - params: DefaultPayload, - response: undefined, - handler() {}, +export const manualService = defineService("tests", { + emptyParamsGetRequest: defineRpc({ + method: "get", + params: undefined, + response: DefaultPayload, + handler() { + return { + message: "ok", + }; + }, + }), + emptyParamsPostRequest: defineRpc({ + params: undefined, + response: DefaultPayload, + handler() { + return { + message: "ok", + }; + }, + }), + emptyResponseGetRequest: defineRpc({ + method: "get", + params: DefaultPayload, + response: undefined, + handler() {}, + }), + emptyResponsePostRequest: defineRpc({ + params: DefaultPayload, + response: undefined, + handler() {}, + }), }); - -router.rpc({ - name: "tests.emptyResponsePostRequest", - params: DefaultPayload, - response: undefined, - handler() {}, -}); - -export default router; diff --git a/tooling/cli/src/common.ts b/tooling/cli/src/common.ts index f9eedd82..7d6956a5 100644 --- a/tooling/cli/src/common.ts +++ b/tooling/cli/src/common.ts @@ -78,7 +78,7 @@ export async function createAppWithRoutesModule(config: ResolvedArriConfig) { ) .join("\n")} - ${routes.map((route) => `app.rpc('${route.name}', ${route.importName});`)} + ${routes.map((route) => `app.rpc('${route.name}', ${route.importName});`).join("\n")} export default app;`, { parser: "typescript", tabWidth: 4 }, From 7c9cb7406f6f8d147a019de200619f9ac0b57ee2 Mon Sep 17 00:00:00 2001 From: joshmossas Date: Fri, 19 Jul 2024 00:55:15 -0500 Subject: [PATCH 5/5] remove references to error messages --- languages/ts/ts-server/README.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/languages/ts/ts-server/README.md b/languages/ts/ts-server/README.md index da21d5d9..c97b802c 100644 --- a/languages/ts/ts-server/README.md +++ b/languages/ts/ts-server/README.md @@ -209,7 +209,6 @@ Event stream procedures make use of [Server Sent Events](https://developer.mozil Arri Event streams sent the following event types: - `message` - A standard message with the response data serialized as JSON -- `error` - An error message with an `ArriRequestError` sent as JSON - `done` - A message to tell clients that there will be no more events - `ping` - A message periodically sent by the server to keep the connection alive. @@ -219,11 +218,6 @@ id: string | undefined; event: "message"; data: Response; // whatever you have specified as the response serialized to json -/// error event /// -id: string | undefined; -event: "error"; -data: ArriRequestError; // serialized to json - /// done event /// event: "done"; data: "this stream has ended";