diff --git a/docs/specs/adx/repo.md b/docs/specs/adx/repo.md index 15c72783802..336dcbca289 100644 --- a/docs/specs/adx/repo.md +++ b/docs/specs/adx/repo.md @@ -133,44 +133,7 @@ To fetch a schema, a request must be sent to the xrpc [`getSchema`](../xrpc.md#g ### Schema structure -Record schemas are encoded in JSON and adhere to the following interface: - -```typescript -interface RecordSchema { - adx: 1 - id: string - revision?: number // a versioning counter - description?: string - record: JSONSchema -} -``` - -Here is an example schema: - -```json -{ - "adx": 1, - "id": "com.example.post", - "record": { - "type": "object", - "required": ["text", "createdAt"], - "properties": { - "text": {"type": "string", "maxLength": 256}, - "createdAt": {"type": "string", "format": "date-time"} - } - } -} -``` - -And here is a record using this example schema: - -```json -{ - "$type": "com.example.post", - "text": "Hello, world!", - "createdAt": "2022-09-15T16:37:17.131Z" -} -``` +Record schemas are encoded in JSON using [Lexicon Schema Documents](../lexicon.md). ### Reserved field names diff --git a/docs/specs/lexicon.md b/docs/specs/lexicon.md new file mode 100644 index 00000000000..c077a31bf13 --- /dev/null +++ b/docs/specs/lexicon.md @@ -0,0 +1,111 @@ +# Lexicon Schema Documents + +Lexicon is a schemas document format used to define [XRPC](./xrpc.md) methods and [ATP Repository](./adx/repo.md) record types. Every Lexicon schema is written in JSON and follows the interface specified below. The schemas are identified using [NSIDs](./nsid.md) which are then used to identify the methods or record types they describe. + +## Interface + +```typescript +interface LexiconDoc { + lexicon: 1 + id: string // an NSID + type: 'query' | 'procedure' | 'record' + revision?: number + description?: string +} + +interface RecordLexiconDoc extends LexiconDoc { + record: JSONSchema +} + +interface XrpcLexiconDoc extends LexiconDoc { + parameters?: Record + input?: XrpcBody + output?: XrpcBody + errors?: XrpcError[] +} + +interface XrpcParameter { + type: 'string' | 'number' | 'integer' | 'boolean' + description?: string + default?: string | number | boolean + required?: boolean + minLength?: number + maxLength?: number + minimum?: number + maximum?: number +} + +interface XrpcBody { + encoding: string|string[] + schema: JSONSchema +} + +interface XrpcError { + name: string + description?: string +} +``` + +## Examples + +### XRPC Method + +```json +{ + "lexicon": 1, + "id": "todo.adx.createAccount", + "type": "procedure", + "description": "Create an account.", + "parameters": {}, + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["email", "username", "password"], + "properties": { + "email": {"type": "string"}, + "username": {"type": "string"}, + "inviteCode": {"type": "string"}, + "password": {"type": "string"} + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["jwt", "name", "did"], + "properties": { + "jwt": { "type": "string" }, + "name": {"type": "string"}, + "did": {"type": "string"} + } + } + }, + "errors": [ + {"name": "InvalidEmail"}, + {"name": "InvalidUsername"}, + {"name": "InvalidPassword"}, + {"name": "InvalidInviteCode"}, + {"name": "UsernameTaken"}, + ] +} +``` + +### ATP Record Type + +```json +{ + "lexicon": 1, + "id": "todo.social.repost", + "type": "record", + "record": { + "type": "object", + "required": ["subject", "createdAt"], + "properties": { + "subject": {"type": "string"}, + "createdAt": {"type": "string", "format": "date-time"} + } + } +} +``` \ No newline at end of file diff --git a/docs/specs/xrpc.md b/docs/specs/xrpc.md index f62261b69e0..87a8001b3c0 100644 --- a/docs/specs/xrpc.md +++ b/docs/specs/xrpc.md @@ -73,82 +73,7 @@ net.users.bob.ping #### Method schemas -Method schemas are encoded in JSON and adhere to the following interface: - -```typescript -interface MethodSchema { - xrpc: 1 - id: string - type: 'query' | 'procedure' - description?: string - parameters?: Record // a map of param names to their definitions - input?: MethodBody - output?: MethodBody -} - -interface MethodParam { - type: 'string' | 'number' | 'integer' | 'boolean' - description?: string - default?: string | number | boolean - required?: boolean - minLength?: number // string only - maxLength?: number // string only - minimum?: number // number and integer only - maximum?: number // number and integer only -} - -interface MethodBody { - encoding: string | string[] // must be a valid mimetype - schema?: JSONSchema // json only -} -``` - -An example query-method schema: - -```json -{ - "xrpc": 1, - "id": "io.social.getFeed", - "type": "query", - "description": "Fetch the user's latest feed.", - "parameters": { - "limit": {"type": "integer", "minimum": 1, "maximum": 50}, - "cursor": {"type": "string"}, - "reverse": {"type": "boolean", "default": true} - }, - "output": { - "encoding": "application/json", - "schema": { - "type": "object", - "required": ["entries", "totalCount"], - "properties": { - "entries": { - "type": "array", - "items": { - "type": "object", - "description": "Entry items will vary and are not constrained at the method level" - } - }, - "totalCount": {"type": "number"} - } - } - } -} -``` - -An example procedure-method schema: - -```json -{ - "xrpc": 1, - "id": "io.social.setProfilePicture", - "type": "procedure", - "description": "Set the user's avatar.", - "input": { - "encoding": ["image/png", "image/jpg"], - } -} -``` +Method schemas are encoded in JSON using [Lexicon Schema Documents](./lexicon.md). #### Schema distribution @@ -211,10 +136,7 @@ The request has succeeded. Expectations: #### `400` Invalid request -The request is invalid and was not processed. Expecations: - -- `Content-Type` header must be `application/json`. -- Response body must match the [InvalidRequest](#invalidrequest) schema. +The request is invalid and was not processed. #### `401` Authentication required @@ -244,10 +166,7 @@ The client has sent too many requests. Rate-limits are decided by each server. E #### `500` Internal server error -The server reached an unexpected condition during processing. Expecations: - -- `Content-Type` header must be `application/json`. -- Response body must match the [InternalError](#internalerror) schema. +The server reached an unexpected condition during processing. #### `501` Method not implemented @@ -255,10 +174,7 @@ The server does not implement the requested method. #### `502` A request to upstream failed -The execution of the procedure depends on a call to another server which has failed. Expecations: - -- `Content-Type` header must be `application/json`. -- Response body must match the [UpstreamError](#upstreamerror) schema. +The execution of the procedure depends on a call to another server which has failed. #### `503` Not enough resources @@ -266,10 +182,7 @@ The server is under heavy load and can't complete the request. #### `504` A request to upstream timed out -The execution of the procedure depends on a call to another server which timed out. Expecations: - -- `Content-Type` header must be `application/json`. -- Response body must match the [UpstreamError](#upstreamerror) schema. +The execution of the procedure depends on a call to another server which timed out. #### Remaining codes @@ -285,36 +198,15 @@ Any response code not explicitly enumerated should be handled as follows: TODO -### Response schemas +### Custom error codes and descriptions -The following schemas are used within the XRPC protocol. - -#### `InvalidRequest` +In non-200 (error) responses, services may respond with a JSON body which matches the following schema: ```typescript -interface InvalidRequest { - error: true - type: 'InvalidRequest' - message: string +interface XrpcErrorDescription { + error?: string + message?: string } ``` -#### `InternalError` - -```typescript -interface InternalError { - error: true - type: 'InternalError' - message: string -} -``` - -#### `UpstreamError` - -```typescript -interface UpstreamError { - error: true - type: 'UpstreamError' - message: string -} -``` \ No newline at end of file +The `error` field of the response body should map to an error name defined in the method's [Lexicon schema](./lexicon.md). This enables more specific error-handling by client software. This is especially advised on 400, 500, and 502 responses where further information will be useful. \ No newline at end of file diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index ad303d2c335..158fd8824a8 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -40,6 +40,40 @@ import * as TodoSocialPost from './types/todo/social/post' import * as TodoSocialProfile from './types/todo/social/profile' import * as TodoSocialRepost from './types/todo/social/repost' +export * as TodoAdxCreateAccount from './types/todo/adx/createAccount' +export * as TodoAdxCreateSession from './types/todo/adx/createSession' +export * as TodoAdxDeleteAccount from './types/todo/adx/deleteAccount' +export * as TodoAdxDeleteSession from './types/todo/adx/deleteSession' +export * as TodoAdxGetAccount from './types/todo/adx/getAccount' +export * as TodoAdxGetAccountsConfig from './types/todo/adx/getAccountsConfig' +export * as TodoAdxGetSession from './types/todo/adx/getSession' +export * as TodoAdxRepoBatchWrite from './types/todo/adx/repoBatchWrite' +export * as TodoAdxRepoCreateRecord from './types/todo/adx/repoCreateRecord' +export * as TodoAdxRepoDeleteRecord from './types/todo/adx/repoDeleteRecord' +export * as TodoAdxRepoDescribe from './types/todo/adx/repoDescribe' +export * as TodoAdxRepoGetRecord from './types/todo/adx/repoGetRecord' +export * as TodoAdxRepoListRecords from './types/todo/adx/repoListRecords' +export * as TodoAdxRepoPutRecord from './types/todo/adx/repoPutRecord' +export * as TodoAdxResolveName from './types/todo/adx/resolveName' +export * as TodoAdxSyncGetRepo from './types/todo/adx/syncGetRepo' +export * as TodoAdxSyncGetRoot from './types/todo/adx/syncGetRoot' +export * as TodoAdxSyncUpdateRepo from './types/todo/adx/syncUpdateRepo' +export * as TodoSocialBadge from './types/todo/social/badge' +export * as TodoSocialFollow from './types/todo/social/follow' +export * as TodoSocialGetFeed from './types/todo/social/getFeed' +export * as TodoSocialGetLikedBy from './types/todo/social/getLikedBy' +export * as TodoSocialGetNotifications from './types/todo/social/getNotifications' +export * as TodoSocialGetPostThread from './types/todo/social/getPostThread' +export * as TodoSocialGetProfile from './types/todo/social/getProfile' +export * as TodoSocialGetRepostedBy from './types/todo/social/getRepostedBy' +export * as TodoSocialGetUserFollowers from './types/todo/social/getUserFollowers' +export * as TodoSocialGetUserFollows from './types/todo/social/getUserFollows' +export * as TodoSocialLike from './types/todo/social/like' +export * as TodoSocialMediaEmbed from './types/todo/social/mediaEmbed' +export * as TodoSocialPost from './types/todo/social/post' +export * as TodoSocialProfile from './types/todo/social/profile' +export * as TodoSocialRepost from './types/todo/social/repost' + export class Client { xrpc: XrpcClient = new XrpcClient() @@ -95,7 +129,11 @@ export class AdxNS { data?: TodoAdxCreateAccount.InputSchema, opts?: TodoAdxCreateAccount.CallOptions ): Promise { - return this._service.xrpc.call('todo.adx.createAccount', params, data, opts) + return this._service.xrpc + .call('todo.adx.createAccount', params, data, opts) + .catch((e) => { + throw TodoAdxCreateAccount.toKnownErr(e) + }) } createSession( @@ -103,7 +141,11 @@ export class AdxNS { data?: TodoAdxCreateSession.InputSchema, opts?: TodoAdxCreateSession.CallOptions ): Promise { - return this._service.xrpc.call('todo.adx.createSession', params, data, opts) + return this._service.xrpc + .call('todo.adx.createSession', params, data, opts) + .catch((e) => { + throw TodoAdxCreateSession.toKnownErr(e) + }) } deleteAccount( @@ -111,7 +153,11 @@ export class AdxNS { data?: TodoAdxDeleteAccount.InputSchema, opts?: TodoAdxDeleteAccount.CallOptions ): Promise { - return this._service.xrpc.call('todo.adx.deleteAccount', params, data, opts) + return this._service.xrpc + .call('todo.adx.deleteAccount', params, data, opts) + .catch((e) => { + throw TodoAdxDeleteAccount.toKnownErr(e) + }) } deleteSession( @@ -119,7 +165,11 @@ export class AdxNS { data?: TodoAdxDeleteSession.InputSchema, opts?: TodoAdxDeleteSession.CallOptions ): Promise { - return this._service.xrpc.call('todo.adx.deleteSession', params, data, opts) + return this._service.xrpc + .call('todo.adx.deleteSession', params, data, opts) + .catch((e) => { + throw TodoAdxDeleteSession.toKnownErr(e) + }) } getAccount( @@ -127,7 +177,11 @@ export class AdxNS { data?: TodoAdxGetAccount.InputSchema, opts?: TodoAdxGetAccount.CallOptions ): Promise { - return this._service.xrpc.call('todo.adx.getAccount', params, data, opts) + return this._service.xrpc + .call('todo.adx.getAccount', params, data, opts) + .catch((e) => { + throw TodoAdxGetAccount.toKnownErr(e) + }) } getAccountsConfig( @@ -135,12 +189,11 @@ export class AdxNS { data?: TodoAdxGetAccountsConfig.InputSchema, opts?: TodoAdxGetAccountsConfig.CallOptions ): Promise { - return this._service.xrpc.call( - 'todo.adx.getAccountsConfig', - params, - data, - opts - ) + return this._service.xrpc + .call('todo.adx.getAccountsConfig', params, data, opts) + .catch((e) => { + throw TodoAdxGetAccountsConfig.toKnownErr(e) + }) } getSession( @@ -148,7 +201,11 @@ export class AdxNS { data?: TodoAdxGetSession.InputSchema, opts?: TodoAdxGetSession.CallOptions ): Promise { - return this._service.xrpc.call('todo.adx.getSession', params, data, opts) + return this._service.xrpc + .call('todo.adx.getSession', params, data, opts) + .catch((e) => { + throw TodoAdxGetSession.toKnownErr(e) + }) } repoBatchWrite( @@ -156,12 +213,11 @@ export class AdxNS { data?: TodoAdxRepoBatchWrite.InputSchema, opts?: TodoAdxRepoBatchWrite.CallOptions ): Promise { - return this._service.xrpc.call( - 'todo.adx.repoBatchWrite', - params, - data, - opts - ) + return this._service.xrpc + .call('todo.adx.repoBatchWrite', params, data, opts) + .catch((e) => { + throw TodoAdxRepoBatchWrite.toKnownErr(e) + }) } repoCreateRecord( @@ -169,12 +225,11 @@ export class AdxNS { data?: TodoAdxRepoCreateRecord.InputSchema, opts?: TodoAdxRepoCreateRecord.CallOptions ): Promise { - return this._service.xrpc.call( - 'todo.adx.repoCreateRecord', - params, - data, - opts - ) + return this._service.xrpc + .call('todo.adx.repoCreateRecord', params, data, opts) + .catch((e) => { + throw TodoAdxRepoCreateRecord.toKnownErr(e) + }) } repoDeleteRecord( @@ -182,12 +237,11 @@ export class AdxNS { data?: TodoAdxRepoDeleteRecord.InputSchema, opts?: TodoAdxRepoDeleteRecord.CallOptions ): Promise { - return this._service.xrpc.call( - 'todo.adx.repoDeleteRecord', - params, - data, - opts - ) + return this._service.xrpc + .call('todo.adx.repoDeleteRecord', params, data, opts) + .catch((e) => { + throw TodoAdxRepoDeleteRecord.toKnownErr(e) + }) } repoDescribe( @@ -195,7 +249,11 @@ export class AdxNS { data?: TodoAdxRepoDescribe.InputSchema, opts?: TodoAdxRepoDescribe.CallOptions ): Promise { - return this._service.xrpc.call('todo.adx.repoDescribe', params, data, opts) + return this._service.xrpc + .call('todo.adx.repoDescribe', params, data, opts) + .catch((e) => { + throw TodoAdxRepoDescribe.toKnownErr(e) + }) } repoGetRecord( @@ -203,7 +261,11 @@ export class AdxNS { data?: TodoAdxRepoGetRecord.InputSchema, opts?: TodoAdxRepoGetRecord.CallOptions ): Promise { - return this._service.xrpc.call('todo.adx.repoGetRecord', params, data, opts) + return this._service.xrpc + .call('todo.adx.repoGetRecord', params, data, opts) + .catch((e) => { + throw TodoAdxRepoGetRecord.toKnownErr(e) + }) } repoListRecords( @@ -211,12 +273,11 @@ export class AdxNS { data?: TodoAdxRepoListRecords.InputSchema, opts?: TodoAdxRepoListRecords.CallOptions ): Promise { - return this._service.xrpc.call( - 'todo.adx.repoListRecords', - params, - data, - opts - ) + return this._service.xrpc + .call('todo.adx.repoListRecords', params, data, opts) + .catch((e) => { + throw TodoAdxRepoListRecords.toKnownErr(e) + }) } repoPutRecord( @@ -224,7 +285,11 @@ export class AdxNS { data?: TodoAdxRepoPutRecord.InputSchema, opts?: TodoAdxRepoPutRecord.CallOptions ): Promise { - return this._service.xrpc.call('todo.adx.repoPutRecord', params, data, opts) + return this._service.xrpc + .call('todo.adx.repoPutRecord', params, data, opts) + .catch((e) => { + throw TodoAdxRepoPutRecord.toKnownErr(e) + }) } resolveName( @@ -232,7 +297,11 @@ export class AdxNS { data?: TodoAdxResolveName.InputSchema, opts?: TodoAdxResolveName.CallOptions ): Promise { - return this._service.xrpc.call('todo.adx.resolveName', params, data, opts) + return this._service.xrpc + .call('todo.adx.resolveName', params, data, opts) + .catch((e) => { + throw TodoAdxResolveName.toKnownErr(e) + }) } syncGetRepo( @@ -240,7 +309,11 @@ export class AdxNS { data?: TodoAdxSyncGetRepo.InputSchema, opts?: TodoAdxSyncGetRepo.CallOptions ): Promise { - return this._service.xrpc.call('todo.adx.syncGetRepo', params, data, opts) + return this._service.xrpc + .call('todo.adx.syncGetRepo', params, data, opts) + .catch((e) => { + throw TodoAdxSyncGetRepo.toKnownErr(e) + }) } syncGetRoot( @@ -248,7 +321,11 @@ export class AdxNS { data?: TodoAdxSyncGetRoot.InputSchema, opts?: TodoAdxSyncGetRoot.CallOptions ): Promise { - return this._service.xrpc.call('todo.adx.syncGetRoot', params, data, opts) + return this._service.xrpc + .call('todo.adx.syncGetRoot', params, data, opts) + .catch((e) => { + throw TodoAdxSyncGetRoot.toKnownErr(e) + }) } syncUpdateRepo( @@ -256,12 +333,11 @@ export class AdxNS { data?: TodoAdxSyncUpdateRepo.InputSchema, opts?: TodoAdxSyncUpdateRepo.CallOptions ): Promise { - return this._service.xrpc.call( - 'todo.adx.syncUpdateRepo', - params, - data, - opts - ) + return this._service.xrpc + .call('todo.adx.syncUpdateRepo', params, data, opts) + .catch((e) => { + throw TodoAdxSyncUpdateRepo.toKnownErr(e) + }) } } @@ -291,7 +367,11 @@ export class SocialNS { data?: TodoSocialGetFeed.InputSchema, opts?: TodoSocialGetFeed.CallOptions ): Promise { - return this._service.xrpc.call('todo.social.getFeed', params, data, opts) + return this._service.xrpc + .call('todo.social.getFeed', params, data, opts) + .catch((e) => { + throw TodoSocialGetFeed.toKnownErr(e) + }) } getLikedBy( @@ -299,7 +379,11 @@ export class SocialNS { data?: TodoSocialGetLikedBy.InputSchema, opts?: TodoSocialGetLikedBy.CallOptions ): Promise { - return this._service.xrpc.call('todo.social.getLikedBy', params, data, opts) + return this._service.xrpc + .call('todo.social.getLikedBy', params, data, opts) + .catch((e) => { + throw TodoSocialGetLikedBy.toKnownErr(e) + }) } getNotifications( @@ -307,12 +391,11 @@ export class SocialNS { data?: TodoSocialGetNotifications.InputSchema, opts?: TodoSocialGetNotifications.CallOptions ): Promise { - return this._service.xrpc.call( - 'todo.social.getNotifications', - params, - data, - opts - ) + return this._service.xrpc + .call('todo.social.getNotifications', params, data, opts) + .catch((e) => { + throw TodoSocialGetNotifications.toKnownErr(e) + }) } getPostThread( @@ -320,12 +403,11 @@ export class SocialNS { data?: TodoSocialGetPostThread.InputSchema, opts?: TodoSocialGetPostThread.CallOptions ): Promise { - return this._service.xrpc.call( - 'todo.social.getPostThread', - params, - data, - opts - ) + return this._service.xrpc + .call('todo.social.getPostThread', params, data, opts) + .catch((e) => { + throw TodoSocialGetPostThread.toKnownErr(e) + }) } getProfile( @@ -333,7 +415,11 @@ export class SocialNS { data?: TodoSocialGetProfile.InputSchema, opts?: TodoSocialGetProfile.CallOptions ): Promise { - return this._service.xrpc.call('todo.social.getProfile', params, data, opts) + return this._service.xrpc + .call('todo.social.getProfile', params, data, opts) + .catch((e) => { + throw TodoSocialGetProfile.toKnownErr(e) + }) } getRepostedBy( @@ -341,12 +427,11 @@ export class SocialNS { data?: TodoSocialGetRepostedBy.InputSchema, opts?: TodoSocialGetRepostedBy.CallOptions ): Promise { - return this._service.xrpc.call( - 'todo.social.getRepostedBy', - params, - data, - opts - ) + return this._service.xrpc + .call('todo.social.getRepostedBy', params, data, opts) + .catch((e) => { + throw TodoSocialGetRepostedBy.toKnownErr(e) + }) } getUserFollowers( @@ -354,12 +439,11 @@ export class SocialNS { data?: TodoSocialGetUserFollowers.InputSchema, opts?: TodoSocialGetUserFollowers.CallOptions ): Promise { - return this._service.xrpc.call( - 'todo.social.getUserFollowers', - params, - data, - opts - ) + return this._service.xrpc + .call('todo.social.getUserFollowers', params, data, opts) + .catch((e) => { + throw TodoSocialGetUserFollowers.toKnownErr(e) + }) } getUserFollows( @@ -367,12 +451,11 @@ export class SocialNS { data?: TodoSocialGetUserFollows.InputSchema, opts?: TodoSocialGetUserFollows.CallOptions ): Promise { - return this._service.xrpc.call( - 'todo.social.getUserFollows', - params, - data, - opts - ) + return this._service.xrpc + .call('todo.social.getUserFollows', params, data, opts) + .catch((e) => { + throw TodoSocialGetUserFollows.toKnownErr(e) + }) } } diff --git a/packages/api/src/schemas.ts b/packages/api/src/schemas.ts index 4716b9c19f3..7ec99982498 100644 --- a/packages/api/src/schemas.ts +++ b/packages/api/src/schemas.ts @@ -40,6 +40,17 @@ export const methodSchemas: MethodSchema[] = [ }, }, }, + errors: [ + { + name: 'InvalidUsername', + }, + { + name: 'InvalidPassword', + }, + { + name: 'UsernameNotAvailable', + }, + ], }, { lexicon: 1, diff --git a/packages/api/src/types/todo/adx/createAccount.ts b/packages/api/src/types/todo/adx/createAccount.ts index 8fe8950a801..02c852beab9 100644 --- a/packages/api/src/types/todo/adx/createAccount.ts +++ b/packages/api/src/types/todo/adx/createAccount.ts @@ -1,7 +1,7 @@ /** * GENERATED CODE - DO NOT MODIFY */ -import { Headers } from '@adxp/xrpc' +import { Headers, XRPCError } from '@adxp/xrpc' export interface QueryParams {} @@ -22,7 +22,34 @@ export interface OutputSchema { export interface Response { success: boolean; - error: boolean; headers: Headers; data: OutputSchema; } + +export class InvalidUsernameError extends XRPCError { + constructor(src: XRPCError) { + super(src.status, src.error, src.message) + } +} + +export class InvalidPasswordError extends XRPCError { + constructor(src: XRPCError) { + super(src.status, src.error, src.message) + } +} + +export class UsernameNotAvailableError extends XRPCError { + constructor(src: XRPCError) { + super(src.status, src.error, src.message) + } +} + +export function toKnownErr(e: any) { + if (e instanceof XRPCError) { + if (e.error === 'InvalidUsername') return new InvalidUsernameError(e) + if (e.error === 'InvalidPassword') return new InvalidPasswordError(e) + if (e.error === 'UsernameNotAvailable') + return new UsernameNotAvailableError(e) + } + return e +} diff --git a/packages/api/src/types/todo/adx/createSession.ts b/packages/api/src/types/todo/adx/createSession.ts index 80159a52a0f..c9c220b7bd2 100644 --- a/packages/api/src/types/todo/adx/createSession.ts +++ b/packages/api/src/types/todo/adx/createSession.ts @@ -1,7 +1,7 @@ /** * GENERATED CODE - DO NOT MODIFY */ -import { Headers } from '@adxp/xrpc' +import { Headers, XRPCError } from '@adxp/xrpc' export interface QueryParams {} @@ -23,7 +23,12 @@ export interface OutputSchema { export interface Response { success: boolean; - error: boolean; headers: Headers; data: OutputSchema; } + +export function toKnownErr(e: any) { + if (e instanceof XRPCError) { + } + return e +} diff --git a/packages/api/src/types/todo/adx/deleteAccount.ts b/packages/api/src/types/todo/adx/deleteAccount.ts index 2b22dfc3aa1..fb8bbc286b1 100644 --- a/packages/api/src/types/todo/adx/deleteAccount.ts +++ b/packages/api/src/types/todo/adx/deleteAccount.ts @@ -1,7 +1,7 @@ /** * GENERATED CODE - DO NOT MODIFY */ -import { Headers } from '@adxp/xrpc' +import { Headers, XRPCError } from '@adxp/xrpc' export interface QueryParams {} @@ -20,7 +20,12 @@ export interface OutputSchema { export interface Response { success: boolean; - error: boolean; headers: Headers; data: OutputSchema; } + +export function toKnownErr(e: any) { + if (e instanceof XRPCError) { + } + return e +} diff --git a/packages/api/src/types/todo/adx/deleteSession.ts b/packages/api/src/types/todo/adx/deleteSession.ts index 2b22dfc3aa1..fb8bbc286b1 100644 --- a/packages/api/src/types/todo/adx/deleteSession.ts +++ b/packages/api/src/types/todo/adx/deleteSession.ts @@ -1,7 +1,7 @@ /** * GENERATED CODE - DO NOT MODIFY */ -import { Headers } from '@adxp/xrpc' +import { Headers, XRPCError } from '@adxp/xrpc' export interface QueryParams {} @@ -20,7 +20,12 @@ export interface OutputSchema { export interface Response { success: boolean; - error: boolean; headers: Headers; data: OutputSchema; } + +export function toKnownErr(e: any) { + if (e instanceof XRPCError) { + } + return e +} diff --git a/packages/api/src/types/todo/adx/getAccount.ts b/packages/api/src/types/todo/adx/getAccount.ts index 2b22dfc3aa1..fb8bbc286b1 100644 --- a/packages/api/src/types/todo/adx/getAccount.ts +++ b/packages/api/src/types/todo/adx/getAccount.ts @@ -1,7 +1,7 @@ /** * GENERATED CODE - DO NOT MODIFY */ -import { Headers } from '@adxp/xrpc' +import { Headers, XRPCError } from '@adxp/xrpc' export interface QueryParams {} @@ -20,7 +20,12 @@ export interface OutputSchema { export interface Response { success: boolean; - error: boolean; headers: Headers; data: OutputSchema; } + +export function toKnownErr(e: any) { + if (e instanceof XRPCError) { + } + return e +} diff --git a/packages/api/src/types/todo/adx/getAccountsConfig.ts b/packages/api/src/types/todo/adx/getAccountsConfig.ts index 88b3441597a..0be791bc447 100644 --- a/packages/api/src/types/todo/adx/getAccountsConfig.ts +++ b/packages/api/src/types/todo/adx/getAccountsConfig.ts @@ -1,7 +1,7 @@ /** * GENERATED CODE - DO NOT MODIFY */ -import { Headers } from '@adxp/xrpc' +import { Headers, XRPCError } from '@adxp/xrpc' export interface QueryParams {} @@ -18,7 +18,12 @@ export interface OutputSchema { export interface Response { success: boolean; - error: boolean; headers: Headers; data: OutputSchema; } + +export function toKnownErr(e: any) { + if (e instanceof XRPCError) { + } + return e +} diff --git a/packages/api/src/types/todo/adx/getSession.ts b/packages/api/src/types/todo/adx/getSession.ts index 8c1632f5e85..49fd535cf25 100644 --- a/packages/api/src/types/todo/adx/getSession.ts +++ b/packages/api/src/types/todo/adx/getSession.ts @@ -1,7 +1,7 @@ /** * GENERATED CODE - DO NOT MODIFY */ -import { Headers } from '@adxp/xrpc' +import { Headers, XRPCError } from '@adxp/xrpc' export interface QueryParams {} @@ -18,7 +18,12 @@ export interface OutputSchema { export interface Response { success: boolean; - error: boolean; headers: Headers; data: OutputSchema; } + +export function toKnownErr(e: any) { + if (e instanceof XRPCError) { + } + return e +} diff --git a/packages/api/src/types/todo/adx/repoBatchWrite.ts b/packages/api/src/types/todo/adx/repoBatchWrite.ts index df5b70e7037..9b5a5e6cc33 100644 --- a/packages/api/src/types/todo/adx/repoBatchWrite.ts +++ b/packages/api/src/types/todo/adx/repoBatchWrite.ts @@ -1,7 +1,7 @@ /** * GENERATED CODE - DO NOT MODIFY */ -import { Headers } from '@adxp/xrpc' +import { Headers, XRPCError } from '@adxp/xrpc' export interface QueryParams { did: string; @@ -40,7 +40,12 @@ export interface OutputSchema { export interface Response { success: boolean; - error: boolean; headers: Headers; data: OutputSchema; } + +export function toKnownErr(e: any) { + if (e instanceof XRPCError) { + } + return e +} diff --git a/packages/api/src/types/todo/adx/repoCreateRecord.ts b/packages/api/src/types/todo/adx/repoCreateRecord.ts index fd3f4a5f20a..0e5368c1336 100644 --- a/packages/api/src/types/todo/adx/repoCreateRecord.ts +++ b/packages/api/src/types/todo/adx/repoCreateRecord.ts @@ -1,7 +1,7 @@ /** * GENERATED CODE - DO NOT MODIFY */ -import { Headers } from '@adxp/xrpc' +import { Headers, XRPCError } from '@adxp/xrpc' export interface QueryParams { did: string; @@ -24,7 +24,12 @@ export interface OutputSchema { export interface Response { success: boolean; - error: boolean; headers: Headers; data: OutputSchema; } + +export function toKnownErr(e: any) { + if (e instanceof XRPCError) { + } + return e +} diff --git a/packages/api/src/types/todo/adx/repoDeleteRecord.ts b/packages/api/src/types/todo/adx/repoDeleteRecord.ts index 807b6c8ca74..70da9512737 100644 --- a/packages/api/src/types/todo/adx/repoDeleteRecord.ts +++ b/packages/api/src/types/todo/adx/repoDeleteRecord.ts @@ -1,7 +1,7 @@ /** * GENERATED CODE - DO NOT MODIFY */ -import { Headers } from '@adxp/xrpc' +import { Headers, XRPCError } from '@adxp/xrpc' export interface QueryParams { did: string; @@ -17,6 +17,11 @@ export type InputSchema = undefined export interface Response { success: boolean; - error: boolean; headers: Headers; } + +export function toKnownErr(e: any) { + if (e instanceof XRPCError) { + } + return e +} diff --git a/packages/api/src/types/todo/adx/repoDescribe.ts b/packages/api/src/types/todo/adx/repoDescribe.ts index 6199d43ed05..3c88d9d3bd9 100644 --- a/packages/api/src/types/todo/adx/repoDescribe.ts +++ b/packages/api/src/types/todo/adx/repoDescribe.ts @@ -1,7 +1,7 @@ /** * GENERATED CODE - DO NOT MODIFY */ -import { Headers } from '@adxp/xrpc' +import { Headers, XRPCError } from '@adxp/xrpc' export interface QueryParams { nameOrDid: string; @@ -23,7 +23,12 @@ export interface OutputSchema { export interface Response { success: boolean; - error: boolean; headers: Headers; data: OutputSchema; } + +export function toKnownErr(e: any) { + if (e instanceof XRPCError) { + } + return e +} diff --git a/packages/api/src/types/todo/adx/repoGetRecord.ts b/packages/api/src/types/todo/adx/repoGetRecord.ts index 6c59f3f948a..6217b9b926f 100644 --- a/packages/api/src/types/todo/adx/repoGetRecord.ts +++ b/packages/api/src/types/todo/adx/repoGetRecord.ts @@ -1,7 +1,7 @@ /** * GENERATED CODE - DO NOT MODIFY */ -import { Headers } from '@adxp/xrpc' +import { Headers, XRPCError } from '@adxp/xrpc' export interface QueryParams { nameOrDid: string; @@ -22,7 +22,12 @@ export interface OutputSchema { export interface Response { success: boolean; - error: boolean; headers: Headers; data: OutputSchema; } + +export function toKnownErr(e: any) { + if (e instanceof XRPCError) { + } + return e +} diff --git a/packages/api/src/types/todo/adx/repoListRecords.ts b/packages/api/src/types/todo/adx/repoListRecords.ts index db27d36b4c1..656857d53e8 100644 --- a/packages/api/src/types/todo/adx/repoListRecords.ts +++ b/packages/api/src/types/todo/adx/repoListRecords.ts @@ -1,7 +1,7 @@ /** * GENERATED CODE - DO NOT MODIFY */ -import { Headers } from '@adxp/xrpc' +import { Headers, XRPCError } from '@adxp/xrpc' export interface QueryParams { nameOrDid: string; @@ -27,7 +27,12 @@ export interface OutputSchema { export interface Response { success: boolean; - error: boolean; headers: Headers; data: OutputSchema; } + +export function toKnownErr(e: any) { + if (e instanceof XRPCError) { + } + return e +} diff --git a/packages/api/src/types/todo/adx/repoPutRecord.ts b/packages/api/src/types/todo/adx/repoPutRecord.ts index 59ac394731d..b2ff531e647 100644 --- a/packages/api/src/types/todo/adx/repoPutRecord.ts +++ b/packages/api/src/types/todo/adx/repoPutRecord.ts @@ -1,7 +1,7 @@ /** * GENERATED CODE - DO NOT MODIFY */ -import { Headers } from '@adxp/xrpc' +import { Headers, XRPCError } from '@adxp/xrpc' export interface QueryParams { did: string; @@ -25,7 +25,12 @@ export interface OutputSchema { export interface Response { success: boolean; - error: boolean; headers: Headers; data: OutputSchema; } + +export function toKnownErr(e: any) { + if (e instanceof XRPCError) { + } + return e +} diff --git a/packages/api/src/types/todo/adx/resolveName.ts b/packages/api/src/types/todo/adx/resolveName.ts index 715d70f86d5..77db5937e2d 100644 --- a/packages/api/src/types/todo/adx/resolveName.ts +++ b/packages/api/src/types/todo/adx/resolveName.ts @@ -1,7 +1,7 @@ /** * GENERATED CODE - DO NOT MODIFY */ -import { Headers } from '@adxp/xrpc' +import { Headers, XRPCError } from '@adxp/xrpc' export interface QueryParams { name?: string; @@ -19,7 +19,12 @@ export interface OutputSchema { export interface Response { success: boolean; - error: boolean; headers: Headers; data: OutputSchema; } + +export function toKnownErr(e: any) { + if (e instanceof XRPCError) { + } + return e +} diff --git a/packages/api/src/types/todo/adx/syncGetRepo.ts b/packages/api/src/types/todo/adx/syncGetRepo.ts index cd48b10b29b..4e9028ff2e4 100644 --- a/packages/api/src/types/todo/adx/syncGetRepo.ts +++ b/packages/api/src/types/todo/adx/syncGetRepo.ts @@ -1,7 +1,7 @@ /** * GENERATED CODE - DO NOT MODIFY */ -import { Headers } from '@adxp/xrpc' +import { Headers, XRPCError } from '@adxp/xrpc' export interface QueryParams { did: string; @@ -16,7 +16,12 @@ export type InputSchema = undefined export interface Response { success: boolean; - error: boolean; headers: Headers; data: Uint8Array; } + +export function toKnownErr(e: any) { + if (e instanceof XRPCError) { + } + return e +} diff --git a/packages/api/src/types/todo/adx/syncGetRoot.ts b/packages/api/src/types/todo/adx/syncGetRoot.ts index 280cef9717f..4e43c4a81a9 100644 --- a/packages/api/src/types/todo/adx/syncGetRoot.ts +++ b/packages/api/src/types/todo/adx/syncGetRoot.ts @@ -1,7 +1,7 @@ /** * GENERATED CODE - DO NOT MODIFY */ -import { Headers } from '@adxp/xrpc' +import { Headers, XRPCError } from '@adxp/xrpc' export interface QueryParams { did: string; @@ -19,7 +19,12 @@ export interface OutputSchema { export interface Response { success: boolean; - error: boolean; headers: Headers; data: OutputSchema; } + +export function toKnownErr(e: any) { + if (e instanceof XRPCError) { + } + return e +} diff --git a/packages/api/src/types/todo/adx/syncUpdateRepo.ts b/packages/api/src/types/todo/adx/syncUpdateRepo.ts index e584e3dd58e..82af1d51110 100644 --- a/packages/api/src/types/todo/adx/syncUpdateRepo.ts +++ b/packages/api/src/types/todo/adx/syncUpdateRepo.ts @@ -1,7 +1,7 @@ /** * GENERATED CODE - DO NOT MODIFY */ -import { Headers } from '@adxp/xrpc' +import { Headers, XRPCError } from '@adxp/xrpc' export interface QueryParams { did: string; @@ -16,6 +16,11 @@ export type InputSchema = string | Uint8Array export interface Response { success: boolean; - error: boolean; headers: Headers; } + +export function toKnownErr(e: any) { + if (e instanceof XRPCError) { + } + return e +} diff --git a/packages/api/src/types/todo/social/getFeed.ts b/packages/api/src/types/todo/social/getFeed.ts index 0d11aad9c59..b59029d47c8 100644 --- a/packages/api/src/types/todo/social/getFeed.ts +++ b/packages/api/src/types/todo/social/getFeed.ts @@ -1,7 +1,7 @@ /** * GENERATED CODE - DO NOT MODIFY */ -import { Headers } from '@adxp/xrpc' +import { Headers, XRPCError } from '@adxp/xrpc' export interface QueryParams { author?: string; @@ -56,7 +56,12 @@ export interface UnknownEmbed { export interface Response { success: boolean; - error: boolean; headers: Headers; data: OutputSchema; } + +export function toKnownErr(e: any) { + if (e instanceof XRPCError) { + } + return e +} diff --git a/packages/api/src/types/todo/social/getLikedBy.ts b/packages/api/src/types/todo/social/getLikedBy.ts index 8a74cc42c98..1999bbe88e2 100644 --- a/packages/api/src/types/todo/social/getLikedBy.ts +++ b/packages/api/src/types/todo/social/getLikedBy.ts @@ -1,7 +1,7 @@ /** * GENERATED CODE - DO NOT MODIFY */ -import { Headers } from '@adxp/xrpc' +import { Headers, XRPCError } from '@adxp/xrpc' export interface QueryParams { uri: string; @@ -28,7 +28,12 @@ export interface OutputSchema { export interface Response { success: boolean; - error: boolean; headers: Headers; data: OutputSchema; } + +export function toKnownErr(e: any) { + if (e instanceof XRPCError) { + } + return e +} diff --git a/packages/api/src/types/todo/social/getNotifications.ts b/packages/api/src/types/todo/social/getNotifications.ts index 31e19829cee..a00dde20d50 100644 --- a/packages/api/src/types/todo/social/getNotifications.ts +++ b/packages/api/src/types/todo/social/getNotifications.ts @@ -1,7 +1,7 @@ /** * GENERATED CODE - DO NOT MODIFY */ -import { Headers } from '@adxp/xrpc' +import { Headers, XRPCError } from '@adxp/xrpc' export interface QueryParams { limit?: number; @@ -31,7 +31,12 @@ export interface Notification { export interface Response { success: boolean; - error: boolean; headers: Headers; data: OutputSchema; } + +export function toKnownErr(e: any) { + if (e instanceof XRPCError) { + } + return e +} diff --git a/packages/api/src/types/todo/social/getPostThread.ts b/packages/api/src/types/todo/social/getPostThread.ts index ad7c5b62f21..6114d0e768b 100644 --- a/packages/api/src/types/todo/social/getPostThread.ts +++ b/packages/api/src/types/todo/social/getPostThread.ts @@ -1,7 +1,7 @@ /** * GENERATED CODE - DO NOT MODIFY */ -import { Headers } from '@adxp/xrpc' +import { Headers, XRPCError } from '@adxp/xrpc' export interface QueryParams { uri: string; @@ -56,7 +56,12 @@ export interface UnknownEmbed { export interface Response { success: boolean; - error: boolean; headers: Headers; data: OutputSchema; } + +export function toKnownErr(e: any) { + if (e instanceof XRPCError) { + } + return e +} diff --git a/packages/api/src/types/todo/social/getProfile.ts b/packages/api/src/types/todo/social/getProfile.ts index 7b18dae4dab..c3fae7c0907 100644 --- a/packages/api/src/types/todo/social/getProfile.ts +++ b/packages/api/src/types/todo/social/getProfile.ts @@ -1,7 +1,7 @@ /** * GENERATED CODE - DO NOT MODIFY */ -import { Headers } from '@adxp/xrpc' +import { Headers, XRPCError } from '@adxp/xrpc' export interface QueryParams { user: string; @@ -42,7 +42,12 @@ export interface Badge { export interface Response { success: boolean; - error: boolean; headers: Headers; data: OutputSchema; } + +export function toKnownErr(e: any) { + if (e instanceof XRPCError) { + } + return e +} diff --git a/packages/api/src/types/todo/social/getRepostedBy.ts b/packages/api/src/types/todo/social/getRepostedBy.ts index eeb8c56c535..a67d853f5c6 100644 --- a/packages/api/src/types/todo/social/getRepostedBy.ts +++ b/packages/api/src/types/todo/social/getRepostedBy.ts @@ -1,7 +1,7 @@ /** * GENERATED CODE - DO NOT MODIFY */ -import { Headers } from '@adxp/xrpc' +import { Headers, XRPCError } from '@adxp/xrpc' export interface QueryParams { uri: string; @@ -28,7 +28,12 @@ export interface OutputSchema { export interface Response { success: boolean; - error: boolean; headers: Headers; data: OutputSchema; } + +export function toKnownErr(e: any) { + if (e instanceof XRPCError) { + } + return e +} diff --git a/packages/api/src/types/todo/social/getUserFollowers.ts b/packages/api/src/types/todo/social/getUserFollowers.ts index 99b71a9e1ef..2d4a897eeb1 100644 --- a/packages/api/src/types/todo/social/getUserFollowers.ts +++ b/packages/api/src/types/todo/social/getUserFollowers.ts @@ -1,7 +1,7 @@ /** * GENERATED CODE - DO NOT MODIFY */ -import { Headers } from '@adxp/xrpc' +import { Headers, XRPCError } from '@adxp/xrpc' export interface QueryParams { user: string; @@ -32,7 +32,12 @@ export interface OutputSchema { export interface Response { success: boolean; - error: boolean; headers: Headers; data: OutputSchema; } + +export function toKnownErr(e: any) { + if (e instanceof XRPCError) { + } + return e +} diff --git a/packages/api/src/types/todo/social/getUserFollows.ts b/packages/api/src/types/todo/social/getUserFollows.ts index d7fda6cecb2..b9d917a3d1b 100644 --- a/packages/api/src/types/todo/social/getUserFollows.ts +++ b/packages/api/src/types/todo/social/getUserFollows.ts @@ -1,7 +1,7 @@ /** * GENERATED CODE - DO NOT MODIFY */ -import { Headers } from '@adxp/xrpc' +import { Headers, XRPCError } from '@adxp/xrpc' export interface QueryParams { user: string; @@ -32,7 +32,12 @@ export interface OutputSchema { export interface Response { success: boolean; - error: boolean; headers: Headers; data: OutputSchema; } + +export function toKnownErr(e: any) { + if (e instanceof XRPCError) { + } + return e +} diff --git a/packages/lex-cli/src/codegen/client.ts b/packages/lex-cli/src/codegen/client.ts index da83fa4f4d0..6b733f6627e 100644 --- a/packages/lex-cli/src/codegen/client.ts +++ b/packages/lex-cli/src/codegen/client.ts @@ -53,13 +53,15 @@ const indexTs = (project: Project, schemas: Schema[], nsidTree: NsidNS[]) => .addImportDeclaration({ moduleSpecifier: './schemas' }) .addNamedImports([{ name: 'methodSchemas' }, { name: 'recordSchemas' }]) - // generate type imports + // generate type imports and re-exports for (const schema of schemas) { + const moduleSpecifier = `./types/${schema.id.split('.').join('/')}` file - .addImportDeclaration({ - moduleSpecifier: `./types/${schema.id.split('.').join('/')}`, - }) + .addImportDeclaration({ moduleSpecifier }) .setNamespaceImport(toTitleCase(schema.id)) + file + .addExportDeclaration({ moduleSpecifier }) + .setNamespaceExport(toTitleCase(schema.id)) } //= export class Client {...} @@ -255,7 +257,13 @@ function genNamespaceCls(file: SourceFile, ns: NsidNS) { type: `${moduleName}.CallOptions`, }) method.setBodyText( - `return this._service.xrpc.call('${schema.id}', params, data, opts)`, + [ + `return this._service.xrpc`, + ` .call('${schema.id}', params, data, opts)`, + ` .catch((e) => {`, + ` throw ${moduleName}.toKnownErr(e)`, + ` })`, + ].join('\n'), ) } @@ -409,13 +417,11 @@ function genRecordCls(file: SourceFile, schema: RecordSchema) { const methodSchemaTs = (project, schema: MethodSchema) => gen(project, `/types/${schema.id.split('.').join('/')}.ts`, async (file) => { - //= import {Headers} from '@adxp/xrpc' + //= import {Headers, XRPCError} from '@adxp/xrpc' const xrpcImport = file.addImportDeclaration({ moduleSpecifier: '@adxp/xrpc', }) - xrpcImport.addNamedImport({ - name: 'Headers', - }) + xrpcImport.addNamedImports([{ name: 'Headers' }, { name: 'XRPCError' }]) //= export interface QueryParams {...} const qp = file.addInterface({ @@ -493,7 +499,6 @@ const methodSchemaTs = (project, schema: MethodSchema) => isExported: true, }) res.addProperty({ name: 'success', type: 'boolean' }) - res.addProperty({ name: 'error', type: 'boolean' }) res.addProperty({ name: 'headers', type: 'Headers' }) if (schema.output?.schema) { if (Array.isArray(schema.output.encoding)) { @@ -504,6 +509,41 @@ const methodSchemaTs = (project, schema: MethodSchema) => } else if (schema.output?.encoding) { res.addProperty({ name: 'data', type: 'Uint8Array' }) } + + // export class {errcode}Error {...} + const customErrors: { name: string; cls: string }[] = [] + for (const error of schema.errors || []) { + let name = toTitleCase(error.name) + if (!name.endsWith('Error')) name += 'Error' + const errCls = file.addClass({ + name, + extends: 'XRPCError', + isExported: true, + }) + errCls + .addConstructor({ + parameters: [{ name: 'src', type: 'XRPCError' }], + }) + .setBodyText(`super(src.status, src.error, src.message)`) + customErrors.push({ name: error.name, cls: name }) + } + + // export function toKnownErr(err: any) {...} + const toKnownErrFn = file.addFunction({ + name: 'toKnownErr', + isExported: true, + }) + toKnownErrFn.addParameter({ name: 'e', type: 'any' }) + toKnownErrFn.setBodyText( + [ + `if (e instanceof XRPCError) {`, + ...customErrors.map( + (err) => `if (e.error === '${err.name}') return new ${err.cls}(e)`, + ), + `}`, + `return e`, + ].join('\n'), + ) }) const recordSchemaTs = (project, schema: RecordSchema) => diff --git a/packages/lex-cli/src/codegen/server.ts b/packages/lex-cli/src/codegen/server.ts index 7b12570529a..249a2e8f5d9 100644 --- a/packages/lex-cli/src/codegen/server.ts +++ b/packages/lex-cli/src/codegen/server.ts @@ -243,43 +243,62 @@ const methodSchemaTs = (project, schema: MethodSchema) => ) } - // export interface HandlerOutput {...} + // export interface HandlerSuccess {...} + let hasHandlerSuccess = false if (schema.output?.schema || schema.output?.encoding) { - const handlerOutput = file.addInterface({ - name: 'HandlerOutput', + hasHandlerSuccess = true + const handlerSuccess = file.addInterface({ + name: 'HandlerSuccess', isExported: true, }) if (Array.isArray(schema.output.encoding)) { - handlerOutput.addProperty({ + handlerSuccess.addProperty({ name: 'encoding', type: schema.output.encoding.map((v) => `'${v}'`).join(' | '), }) } else if (typeof schema.output.encoding === 'string') { - handlerOutput.addProperty({ + handlerSuccess.addProperty({ name: 'encoding', type: `'${schema.output.encoding}'`, }) } if (schema.output?.schema) { if (Array.isArray(schema.output.encoding)) { - handlerOutput.addProperty({ + handlerSuccess.addProperty({ name: 'body', type: 'OutputSchema | Uint8Array', }) } else { - handlerOutput.addProperty({ name: 'body', type: 'OutputSchema' }) + handlerSuccess.addProperty({ name: 'body', type: 'OutputSchema' }) } } else if (schema.output?.encoding) { - handlerOutput.addProperty({ name: 'body', type: 'Uint8Array' }) + handlerSuccess.addProperty({ name: 'body', type: 'Uint8Array' }) } - } else { - file.addTypeAlias({ - isExported: true, - name: 'HandlerOutput', - type: 'void', + } + + // export interface HandlerError {...} + const handlerError = file.addInterface({ + name: 'HandlerError', + isExported: true, + }) + handlerError.addProperties([ + { name: 'status', type: 'number' }, + { name: 'message?', type: 'string' }, + ]) + if (schema.errors?.length) { + handlerError.addProperty({ + name: 'error?', + type: schema.errors.map((err) => `'${err.name}'`).join(' | '), }) } + // export type HandlerOutput = ... + file.addTypeAlias({ + isExported: true, + name: 'HandlerOutput', + type: `HandlerError | ${hasHandlerSuccess ? 'HandlerSuccess' : 'void'}`, + }) + //= export interface OutputSchema {...} if (schema.output?.schema) { file.insertText( diff --git a/packages/lexicon/src/types.ts b/packages/lexicon/src/types.ts index ebf34ca514d..84025b37682 100644 --- a/packages/lexicon/src/types.ts +++ b/packages/lexicon/src/types.ts @@ -43,6 +43,12 @@ export const methodSchemaParam = z.object({ }) export type MethodSchemaParam = z.infer +export const methodSchemaError = z.object({ + name: z.string(), + description: z.string().optional(), +}) +export type MethodSchemaError = z.infer + export const methodSchema = z.object({ lexicon: z.literal(1), id: z.string(), @@ -51,6 +57,7 @@ export const methodSchema = z.object({ parameters: z.record(methodSchemaParam).optional(), input: methodSchemaBody.optional(), output: methodSchemaBody.optional(), + errors: methodSchemaError.array().optional(), }) export type MethodSchema = z.infer diff --git a/packages/server/src/api/todo/adx/account.ts b/packages/server/src/api/todo/adx/account.ts index b52c439803b..cb5e315bda8 100644 --- a/packages/server/src/api/todo/adx/account.ts +++ b/packages/server/src/api/todo/adx/account.ts @@ -1,4 +1,5 @@ import { Server } from '../../../lexicon' +import * as CreateAccount from '../../../lexicon/types/todo/adx/createAccount' import { InvalidRequestError } from '@adxp/xrpc-server' import * as util from '../../../util' import { Repo } from '@adxp/repo' @@ -33,14 +34,17 @@ export default function (server: Server) { const cfg = util.getConfig(res) if (username.startsWith('did:')) { - throw new InvalidRequestError( - 'Cannot register a username that starts with `did:`', - ) + return { + status: 400, + error: 'InvalidUsername', + message: 'Cannot register a username that starts with `did:`', + } } if (!did.startsWith('did:')) { - throw new InvalidRequestError( - 'Cannot register a did that does not start with `did:`', - ) + return { + status: 400, + message: 'Cannot register a did that does not start with `did:`', + } } let isTestUser = false diff --git a/packages/server/src/lexicon/schemas.ts b/packages/server/src/lexicon/schemas.ts index 4716b9c19f3..7ec99982498 100644 --- a/packages/server/src/lexicon/schemas.ts +++ b/packages/server/src/lexicon/schemas.ts @@ -40,6 +40,17 @@ export const methodSchemas: MethodSchema[] = [ }, }, }, + errors: [ + { + name: 'InvalidUsername', + }, + { + name: 'InvalidPassword', + }, + { + name: 'UsernameNotAvailable', + }, + ], }, { lexicon: 1, diff --git a/packages/server/src/lexicon/types/todo/adx/createAccount.ts b/packages/server/src/lexicon/types/todo/adx/createAccount.ts index eeaa02e74d8..70449ab4dca 100644 --- a/packages/server/src/lexicon/types/todo/adx/createAccount.ts +++ b/packages/server/src/lexicon/types/todo/adx/createAccount.ts @@ -16,11 +16,19 @@ export interface InputSchema { password: string; } -export interface HandlerOutput { +export interface HandlerSuccess { encoding: 'application/json'; body: OutputSchema; } +export interface HandlerError { + status: number; + message?: string; + error?: 'InvalidUsername' | 'InvalidPassword' | 'UsernameNotAvailable'; +} + +export type HandlerOutput = HandlerError | HandlerSuccess + export interface OutputSchema { jwt: string; } diff --git a/packages/server/src/lexicon/types/todo/adx/createSession.ts b/packages/server/src/lexicon/types/todo/adx/createSession.ts index cf4777afcd6..79a34da9892 100644 --- a/packages/server/src/lexicon/types/todo/adx/createSession.ts +++ b/packages/server/src/lexicon/types/todo/adx/createSession.ts @@ -15,11 +15,18 @@ export interface InputSchema { password: string; } -export interface HandlerOutput { +export interface HandlerSuccess { encoding: 'application/json'; body: OutputSchema; } +export interface HandlerError { + status: number; + message?: string; +} + +export type HandlerOutput = HandlerError | HandlerSuccess + export interface OutputSchema { jwt: string; name: string; diff --git a/packages/server/src/lexicon/types/todo/adx/deleteAccount.ts b/packages/server/src/lexicon/types/todo/adx/deleteAccount.ts index 758412f04cf..c3f89fb1f07 100644 --- a/packages/server/src/lexicon/types/todo/adx/deleteAccount.ts +++ b/packages/server/src/lexicon/types/todo/adx/deleteAccount.ts @@ -11,11 +11,18 @@ export interface InputSchema { [k: string]: unknown; } -export interface HandlerOutput { +export interface HandlerSuccess { encoding: ''; body: OutputSchema; } +export interface HandlerError { + status: number; + message?: string; +} + +export type HandlerOutput = HandlerError | HandlerSuccess + export interface OutputSchema { [k: string]: unknown; } diff --git a/packages/server/src/lexicon/types/todo/adx/deleteSession.ts b/packages/server/src/lexicon/types/todo/adx/deleteSession.ts index 758412f04cf..c3f89fb1f07 100644 --- a/packages/server/src/lexicon/types/todo/adx/deleteSession.ts +++ b/packages/server/src/lexicon/types/todo/adx/deleteSession.ts @@ -11,11 +11,18 @@ export interface InputSchema { [k: string]: unknown; } -export interface HandlerOutput { +export interface HandlerSuccess { encoding: ''; body: OutputSchema; } +export interface HandlerError { + status: number; + message?: string; +} + +export type HandlerOutput = HandlerError | HandlerSuccess + export interface OutputSchema { [k: string]: unknown; } diff --git a/packages/server/src/lexicon/types/todo/adx/getAccount.ts b/packages/server/src/lexicon/types/todo/adx/getAccount.ts index 758412f04cf..c3f89fb1f07 100644 --- a/packages/server/src/lexicon/types/todo/adx/getAccount.ts +++ b/packages/server/src/lexicon/types/todo/adx/getAccount.ts @@ -11,11 +11,18 @@ export interface InputSchema { [k: string]: unknown; } -export interface HandlerOutput { +export interface HandlerSuccess { encoding: ''; body: OutputSchema; } +export interface HandlerError { + status: number; + message?: string; +} + +export type HandlerOutput = HandlerError | HandlerSuccess + export interface OutputSchema { [k: string]: unknown; } diff --git a/packages/server/src/lexicon/types/todo/adx/getAccountsConfig.ts b/packages/server/src/lexicon/types/todo/adx/getAccountsConfig.ts index 893b922e518..fb6174b471a 100644 --- a/packages/server/src/lexicon/types/todo/adx/getAccountsConfig.ts +++ b/packages/server/src/lexicon/types/todo/adx/getAccountsConfig.ts @@ -7,11 +7,18 @@ export interface QueryParams {} export type HandlerInput = undefined -export interface HandlerOutput { +export interface HandlerSuccess { encoding: 'application/json'; body: OutputSchema; } +export interface HandlerError { + status: number; + message?: string; +} + +export type HandlerOutput = HandlerError | HandlerSuccess + export interface OutputSchema { inviteCodeRequired?: boolean; availableUserDomains: string[]; diff --git a/packages/server/src/lexicon/types/todo/adx/getSession.ts b/packages/server/src/lexicon/types/todo/adx/getSession.ts index bf4eef9f7ba..6d6e7e93ac4 100644 --- a/packages/server/src/lexicon/types/todo/adx/getSession.ts +++ b/packages/server/src/lexicon/types/todo/adx/getSession.ts @@ -7,11 +7,18 @@ export interface QueryParams {} export type HandlerInput = undefined -export interface HandlerOutput { +export interface HandlerSuccess { encoding: 'application/json'; body: OutputSchema; } +export interface HandlerError { + status: number; + message?: string; +} + +export type HandlerOutput = HandlerError | HandlerSuccess + export interface OutputSchema { name: string; did: string; diff --git a/packages/server/src/lexicon/types/todo/adx/repoBatchWrite.ts b/packages/server/src/lexicon/types/todo/adx/repoBatchWrite.ts index 0d341ba6368..fe180ae42a2 100644 --- a/packages/server/src/lexicon/types/todo/adx/repoBatchWrite.ts +++ b/packages/server/src/lexicon/types/todo/adx/repoBatchWrite.ts @@ -34,11 +34,18 @@ export interface InputSchema { )[]; } -export interface HandlerOutput { +export interface HandlerSuccess { encoding: 'application/json'; body: OutputSchema; } +export interface HandlerError { + status: number; + message?: string; +} + +export type HandlerOutput = HandlerError | HandlerSuccess + export interface OutputSchema { [k: string]: unknown; } diff --git a/packages/server/src/lexicon/types/todo/adx/repoCreateRecord.ts b/packages/server/src/lexicon/types/todo/adx/repoCreateRecord.ts index 8fab8f8acaf..55f7be34161 100644 --- a/packages/server/src/lexicon/types/todo/adx/repoCreateRecord.ts +++ b/packages/server/src/lexicon/types/todo/adx/repoCreateRecord.ts @@ -18,11 +18,18 @@ export interface InputSchema { [k: string]: unknown; } -export interface HandlerOutput { +export interface HandlerSuccess { encoding: 'application/json'; body: OutputSchema; } +export interface HandlerError { + status: number; + message?: string; +} + +export type HandlerOutput = HandlerError | HandlerSuccess + export interface OutputSchema { uri: string; } diff --git a/packages/server/src/lexicon/types/todo/adx/repoDeleteRecord.ts b/packages/server/src/lexicon/types/todo/adx/repoDeleteRecord.ts index a3d0897329e..31277eff57b 100644 --- a/packages/server/src/lexicon/types/todo/adx/repoDeleteRecord.ts +++ b/packages/server/src/lexicon/types/todo/adx/repoDeleteRecord.ts @@ -10,7 +10,13 @@ export interface QueryParams { } export type HandlerInput = undefined -export type HandlerOutput = void + +export interface HandlerError { + status: number; + message?: string; +} + +export type HandlerOutput = HandlerError | void export type Handler = ( params: QueryParams, input: HandlerInput, diff --git a/packages/server/src/lexicon/types/todo/adx/repoDescribe.ts b/packages/server/src/lexicon/types/todo/adx/repoDescribe.ts index 7260d9242ce..bd45d598cee 100644 --- a/packages/server/src/lexicon/types/todo/adx/repoDescribe.ts +++ b/packages/server/src/lexicon/types/todo/adx/repoDescribe.ts @@ -9,11 +9,18 @@ export interface QueryParams { export type HandlerInput = undefined -export interface HandlerOutput { +export interface HandlerSuccess { encoding: 'application/json'; body: OutputSchema; } +export interface HandlerError { + status: number; + message?: string; +} + +export type HandlerOutput = HandlerError | HandlerSuccess + export interface OutputSchema { name: string; did: string; diff --git a/packages/server/src/lexicon/types/todo/adx/repoGetRecord.ts b/packages/server/src/lexicon/types/todo/adx/repoGetRecord.ts index 0f554bcad50..e2da9b57ab0 100644 --- a/packages/server/src/lexicon/types/todo/adx/repoGetRecord.ts +++ b/packages/server/src/lexicon/types/todo/adx/repoGetRecord.ts @@ -11,11 +11,18 @@ export interface QueryParams { export type HandlerInput = undefined -export interface HandlerOutput { +export interface HandlerSuccess { encoding: 'application/json'; body: OutputSchema; } +export interface HandlerError { + status: number; + message?: string; +} + +export type HandlerOutput = HandlerError | HandlerSuccess + export interface OutputSchema { uri: string; value: {}; diff --git a/packages/server/src/lexicon/types/todo/adx/repoListRecords.ts b/packages/server/src/lexicon/types/todo/adx/repoListRecords.ts index 0a10835a537..6e318aa359d 100644 --- a/packages/server/src/lexicon/types/todo/adx/repoListRecords.ts +++ b/packages/server/src/lexicon/types/todo/adx/repoListRecords.ts @@ -14,11 +14,18 @@ export interface QueryParams { export type HandlerInput = undefined -export interface HandlerOutput { +export interface HandlerSuccess { encoding: 'application/json'; body: OutputSchema; } +export interface HandlerError { + status: number; + message?: string; +} + +export type HandlerOutput = HandlerError | HandlerSuccess + export interface OutputSchema { records: { uri: string, diff --git a/packages/server/src/lexicon/types/todo/adx/repoPutRecord.ts b/packages/server/src/lexicon/types/todo/adx/repoPutRecord.ts index 8d049dea65d..c8e95a9b960 100644 --- a/packages/server/src/lexicon/types/todo/adx/repoPutRecord.ts +++ b/packages/server/src/lexicon/types/todo/adx/repoPutRecord.ts @@ -19,11 +19,18 @@ export interface InputSchema { [k: string]: unknown; } -export interface HandlerOutput { +export interface HandlerSuccess { encoding: 'application/json'; body: OutputSchema; } +export interface HandlerError { + status: number; + message?: string; +} + +export type HandlerOutput = HandlerError | HandlerSuccess + export interface OutputSchema { uri: string; } diff --git a/packages/server/src/lexicon/types/todo/adx/resolveName.ts b/packages/server/src/lexicon/types/todo/adx/resolveName.ts index 3b7127296e4..60bd090438e 100644 --- a/packages/server/src/lexicon/types/todo/adx/resolveName.ts +++ b/packages/server/src/lexicon/types/todo/adx/resolveName.ts @@ -9,11 +9,18 @@ export interface QueryParams { export type HandlerInput = undefined -export interface HandlerOutput { +export interface HandlerSuccess { encoding: 'application/json'; body: OutputSchema; } +export interface HandlerError { + status: number; + message?: string; +} + +export type HandlerOutput = HandlerError | HandlerSuccess + export interface OutputSchema { did: string; } diff --git a/packages/server/src/lexicon/types/todo/adx/syncGetRepo.ts b/packages/server/src/lexicon/types/todo/adx/syncGetRepo.ts index d00f2e894fa..cac491e6d79 100644 --- a/packages/server/src/lexicon/types/todo/adx/syncGetRepo.ts +++ b/packages/server/src/lexicon/types/todo/adx/syncGetRepo.ts @@ -10,11 +10,17 @@ export interface QueryParams { export type HandlerInput = undefined -export interface HandlerOutput { +export interface HandlerSuccess { encoding: 'application/cbor'; body: Uint8Array; } +export interface HandlerError { + status: number; + message?: string; +} + +export type HandlerOutput = HandlerError | HandlerSuccess export type Handler = ( params: QueryParams, input: HandlerInput, diff --git a/packages/server/src/lexicon/types/todo/adx/syncGetRoot.ts b/packages/server/src/lexicon/types/todo/adx/syncGetRoot.ts index 1850002a9f6..cc8d0dd3685 100644 --- a/packages/server/src/lexicon/types/todo/adx/syncGetRoot.ts +++ b/packages/server/src/lexicon/types/todo/adx/syncGetRoot.ts @@ -9,11 +9,18 @@ export interface QueryParams { export type HandlerInput = undefined -export interface HandlerOutput { +export interface HandlerSuccess { encoding: 'application/json'; body: OutputSchema; } +export interface HandlerError { + status: number; + message?: string; +} + +export type HandlerOutput = HandlerError | HandlerSuccess + export interface OutputSchema { root: string; } diff --git a/packages/server/src/lexicon/types/todo/adx/syncUpdateRepo.ts b/packages/server/src/lexicon/types/todo/adx/syncUpdateRepo.ts index c170764f40f..1470b2ad699 100644 --- a/packages/server/src/lexicon/types/todo/adx/syncUpdateRepo.ts +++ b/packages/server/src/lexicon/types/todo/adx/syncUpdateRepo.ts @@ -12,7 +12,12 @@ export interface HandlerInput { body: Uint8Array; } -export type HandlerOutput = void +export interface HandlerError { + status: number; + message?: string; +} + +export type HandlerOutput = HandlerError | void export type Handler = ( params: QueryParams, input: HandlerInput, diff --git a/packages/server/src/lexicon/types/todo/social/getFeed.ts b/packages/server/src/lexicon/types/todo/social/getFeed.ts index f40ffa4e180..b8d940fc866 100644 --- a/packages/server/src/lexicon/types/todo/social/getFeed.ts +++ b/packages/server/src/lexicon/types/todo/social/getFeed.ts @@ -11,11 +11,18 @@ export interface QueryParams { export type HandlerInput = undefined -export interface HandlerOutput { +export interface HandlerSuccess { encoding: 'application/json'; body: OutputSchema; } +export interface HandlerError { + status: number; + message?: string; +} + +export type HandlerOutput = HandlerError | HandlerSuccess + export interface OutputSchema { feed: FeedItem[]; } diff --git a/packages/server/src/lexicon/types/todo/social/getLikedBy.ts b/packages/server/src/lexicon/types/todo/social/getLikedBy.ts index 32936431ba4..2298dac6f9a 100644 --- a/packages/server/src/lexicon/types/todo/social/getLikedBy.ts +++ b/packages/server/src/lexicon/types/todo/social/getLikedBy.ts @@ -11,11 +11,18 @@ export interface QueryParams { export type HandlerInput = undefined -export interface HandlerOutput { +export interface HandlerSuccess { encoding: 'application/json'; body: OutputSchema; } +export interface HandlerError { + status: number; + message?: string; +} + +export type HandlerOutput = HandlerError | HandlerSuccess + export interface OutputSchema { uri: string; likedBy: { diff --git a/packages/server/src/lexicon/types/todo/social/getNotifications.ts b/packages/server/src/lexicon/types/todo/social/getNotifications.ts index b27042dea0b..cda12676fa0 100644 --- a/packages/server/src/lexicon/types/todo/social/getNotifications.ts +++ b/packages/server/src/lexicon/types/todo/social/getNotifications.ts @@ -10,11 +10,18 @@ export interface QueryParams { export type HandlerInput = undefined -export interface HandlerOutput { +export interface HandlerSuccess { encoding: 'application/json'; body: OutputSchema; } +export interface HandlerError { + status: number; + message?: string; +} + +export type HandlerOutput = HandlerError | HandlerSuccess + export interface OutputSchema { notifications: Notification[]; } diff --git a/packages/server/src/lexicon/types/todo/social/getPostThread.ts b/packages/server/src/lexicon/types/todo/social/getPostThread.ts index a26991438e7..1be86dee04e 100644 --- a/packages/server/src/lexicon/types/todo/social/getPostThread.ts +++ b/packages/server/src/lexicon/types/todo/social/getPostThread.ts @@ -10,11 +10,18 @@ export interface QueryParams { export type HandlerInput = undefined -export interface HandlerOutput { +export interface HandlerSuccess { encoding: 'application/json'; body: OutputSchema; } +export interface HandlerError { + status: number; + message?: string; +} + +export type HandlerOutput = HandlerError | HandlerSuccess + export interface OutputSchema { thread: Post; } diff --git a/packages/server/src/lexicon/types/todo/social/getProfile.ts b/packages/server/src/lexicon/types/todo/social/getProfile.ts index 97fc418ad17..c3734bbaf41 100644 --- a/packages/server/src/lexicon/types/todo/social/getProfile.ts +++ b/packages/server/src/lexicon/types/todo/social/getProfile.ts @@ -9,11 +9,18 @@ export interface QueryParams { export type HandlerInput = undefined -export interface HandlerOutput { +export interface HandlerSuccess { encoding: 'application/json'; body: OutputSchema; } +export interface HandlerError { + status: number; + message?: string; +} + +export type HandlerOutput = HandlerError | HandlerSuccess + export interface OutputSchema { did: string; name: string; diff --git a/packages/server/src/lexicon/types/todo/social/getRepostedBy.ts b/packages/server/src/lexicon/types/todo/social/getRepostedBy.ts index d6326750948..94283117241 100644 --- a/packages/server/src/lexicon/types/todo/social/getRepostedBy.ts +++ b/packages/server/src/lexicon/types/todo/social/getRepostedBy.ts @@ -11,11 +11,18 @@ export interface QueryParams { export type HandlerInput = undefined -export interface HandlerOutput { +export interface HandlerSuccess { encoding: 'application/json'; body: OutputSchema; } +export interface HandlerError { + status: number; + message?: string; +} + +export type HandlerOutput = HandlerError | HandlerSuccess + export interface OutputSchema { uri: string; repostedBy: { diff --git a/packages/server/src/lexicon/types/todo/social/getUserFollowers.ts b/packages/server/src/lexicon/types/todo/social/getUserFollowers.ts index e66bdd39b46..404de03815d 100644 --- a/packages/server/src/lexicon/types/todo/social/getUserFollowers.ts +++ b/packages/server/src/lexicon/types/todo/social/getUserFollowers.ts @@ -11,11 +11,18 @@ export interface QueryParams { export type HandlerInput = undefined -export interface HandlerOutput { +export interface HandlerSuccess { encoding: 'application/json'; body: OutputSchema; } +export interface HandlerError { + status: number; + message?: string; +} + +export type HandlerOutput = HandlerError | HandlerSuccess + export interface OutputSchema { subject: { did: string, diff --git a/packages/server/src/lexicon/types/todo/social/getUserFollows.ts b/packages/server/src/lexicon/types/todo/social/getUserFollows.ts index 573ee507cc6..c3e908aa06f 100644 --- a/packages/server/src/lexicon/types/todo/social/getUserFollows.ts +++ b/packages/server/src/lexicon/types/todo/social/getUserFollows.ts @@ -11,11 +11,18 @@ export interface QueryParams { export type HandlerInput = undefined -export interface HandlerOutput { +export interface HandlerSuccess { encoding: 'application/json'; body: OutputSchema; } +export interface HandlerError { + status: number; + message?: string; +} + +export type HandlerOutput = HandlerError | HandlerSuccess + export interface OutputSchema { subject: { did: string, diff --git a/packages/server/tests/account.test.ts b/packages/server/tests/account.test.ts index ee00c67cf34..715f0fe2874 100644 --- a/packages/server/tests/account.test.ts +++ b/packages/server/tests/account.test.ts @@ -1,4 +1,7 @@ -import AdxApi, { ServiceClient as AdxServiceClient } from '@adxp/api' +import AdxApi, { + ServiceClient as AdxServiceClient, + TodoAdxCreateAccount, +} from '@adxp/api' import * as util from './_util' const username = 'alice.test' @@ -33,6 +36,24 @@ describe('auth', () => { expect(typeof res.data.jwt).toBe('string') }) + it('fails on invalid usernames', async () => { + try { + await client.todo.adx.createAccount( + {}, + { + username: 'did:bad-username.test', + did: 'bad.test', + password: 'asdf', + }, + ) + throw new Error('Didnt throw') + } catch (e: any) { + expect( + e instanceof TodoAdxCreateAccount.InvalidUsernameError, + ).toBeTruthy() + } + }) + it('fails on authenticated requests', async () => { await expect(client.todo.adx.getSession({})).rejects.toThrow() }) diff --git a/packages/xrpc-server/src/server.ts b/packages/xrpc-server/src/server.ts index d27ce153e31..ace23ea16fd 100644 --- a/packages/xrpc-server/src/server.ts +++ b/packages/xrpc-server/src/server.ts @@ -1,7 +1,16 @@ import express from 'express' import { ValidateFunction } from 'ajv' import { MethodSchema, methodSchema, isValidMethodSchema } from '@adxp/lexicon' -import { XRPCHandler, XRPCError, InvalidRequestError } from './types' +import { + XRPCHandler, + XRPCError, + InvalidRequestError, + HandlerOutput, + HandlerSuccess, + handlerSuccess, + HandlerError, + handlerError, +} from './types' import { ajv, validateReqParams, @@ -113,37 +122,43 @@ export class Server { // run the handler const outputUnvalidated = await handler(params, input, req, res) - // validate response - const output = validateOutput( - schema, - outputUnvalidated, - this.outputValidators.get(schema.id), - ) + if (!outputUnvalidated || isHandlerSuccess(outputUnvalidated)) { + // validate response + const output = validateOutput( + schema, + outputUnvalidated, + this.outputValidators.get(schema.id), + ) - // send response - if ( - output?.encoding === 'application/json' || - output?.encoding === 'json' - ) { - res.status(200).json(output.body) - } else if (output) { - res.header('Content-Type', output.encoding) - res - .status(200) - .send( - output.body instanceof Uint8Array - ? Buffer.from(output.body) - : output.body, - ) - } else { - res.status(200).end() + // send response + if ( + output?.encoding === 'application/json' || + output?.encoding === 'json' + ) { + res.status(200).json(output.body) + } else if (output) { + res.header('Content-Type', output.encoding) + res + .status(200) + .send( + output.body instanceof Uint8Array + ? Buffer.from(output.body) + : output.body, + ) + } else { + res.status(200).end() + } + } else if (isHandlerError(outputUnvalidated)) { + return res.status(outputUnvalidated.status).json({ + error: outputUnvalidated.error, + message: outputUnvalidated.message, + }) } } catch (e: any) { if (e instanceof XRPCError) { res.status(e.type).json({ - error: true, - type: e.typeStr, - message: e.message || e.typeStr, + error: e.customErrorName, + message: e.errorMessage || e.typeStr, }) } else { console.error( @@ -151,11 +166,17 @@ export class Server { ) console.error(e) res.status(500).json({ - error: true, - type: 'InternalError', message: 'Unexpected internal server error', }) } } } } + +function isHandlerSuccess(v: HandlerOutput): v is HandlerSuccess { + return handlerSuccess.safeParse(v).success +} + +function isHandlerError(v: HandlerOutput): v is HandlerError { + return handlerError.safeParse(v).success +} diff --git a/packages/xrpc-server/src/types.ts b/packages/xrpc-server/src/types.ts index 4d8bbcfe36d..67428e417dd 100644 --- a/packages/xrpc-server/src/types.ts +++ b/packages/xrpc-server/src/types.ts @@ -10,11 +10,20 @@ export const handlerInput = zod.object({ }) export type HandlerInput = zod.infer -export const handlerOutput = zod.object({ +export const handlerSuccess = zod.object({ encoding: zod.string(), body: zod.any(), }) -export type HandlerOutput = zod.infer +export type HandlerSuccess = zod.infer + +export const handlerError = zod.object({ + status: zod.number(), + error: zod.string().optional(), + message: zod.string().optional(), +}) +export type HandlerError = zod.infer + +export type HandlerOutput = HandlerSuccess | HandlerError export type XRPCHandler = ( params: Params, @@ -24,8 +33,12 @@ export type XRPCHandler = ( ) => Promise | HandlerOutput | undefined export class XRPCError extends Error { - constructor(public type: ResponseType, message?: string) { - super(message) + constructor( + public type: ResponseType, + public errorMessage?: string, + public customErrorName?: string, + ) { + super(errorMessage) } get typeStr() { @@ -34,43 +47,43 @@ export class XRPCError extends Error { } export class InvalidRequestError extends XRPCError { - constructor(message?: string) { - super(ResponseType.InvalidRequest, message) + constructor(errorMessage?: string, customErrorName?: string) { + super(ResponseType.InvalidRequest, errorMessage, customErrorName) } } export class AuthRequiredError extends XRPCError { - constructor(message?: string) { - super(ResponseType.AuthRequired, message) + constructor(errorMessage?: string, customErrorName?: string) { + super(ResponseType.AuthRequired, errorMessage, customErrorName) } } export class ForbiddenError extends XRPCError { - constructor(message?: string) { - super(ResponseType.Forbidden, message) + constructor(errorMessage?: string, customErrorName?: string) { + super(ResponseType.Forbidden, errorMessage, customErrorName) } } export class InternalServerError extends XRPCError { - constructor(message?: string) { - super(ResponseType.InternalServerError, message) + constructor(errorMessage?: string, customErrorName?: string) { + super(ResponseType.InternalServerError, errorMessage, customErrorName) } } export class UpstreamFailureError extends XRPCError { - constructor(message?: string) { - super(ResponseType.UpstreamFailure, message) + constructor(errorMessage?: string, customErrorName?: string) { + super(ResponseType.UpstreamFailure, errorMessage, customErrorName) } } export class NotEnoughResoucesError extends XRPCError { - constructor(message?: string) { - super(ResponseType.NotEnoughResouces, message) + constructor(errorMessage?: string, customErrorName?: string) { + super(ResponseType.NotEnoughResouces, errorMessage, customErrorName) } } export class UpstreamTimeoutError extends XRPCError { - constructor(message?: string) { - super(ResponseType.UpstreamTimeout, message) + constructor(errorMessage?: string, customErrorName?: string) { + super(ResponseType.UpstreamTimeout, errorMessage, customErrorName) } } diff --git a/packages/xrpc-server/src/util.ts b/packages/xrpc-server/src/util.ts index d26a6241d9e..be8afbce794 100644 --- a/packages/xrpc-server/src/util.ts +++ b/packages/xrpc-server/src/util.ts @@ -6,8 +6,8 @@ import addFormats from 'ajv-formats' import { Params, HandlerInput, - HandlerOutput, - handlerOutput, + HandlerSuccess, + handlerSuccess, InvalidRequestError, InternalServerError, } from './types' @@ -144,12 +144,12 @@ export function validateInput( export function validateOutput( schema: MethodSchema, - output: HandlerOutput | undefined, + output: HandlerSuccess | undefined, jsonValidator?: ValidateFunction, -): HandlerOutput | undefined { +): HandlerSuccess | undefined { // initial validation if (output) { - handlerOutput.parse(output) + handlerSuccess.parse(output) } // response expectation diff --git a/packages/xrpc-server/tests/errors.test.ts b/packages/xrpc-server/tests/errors.test.ts new file mode 100644 index 00000000000..7d566940f46 --- /dev/null +++ b/packages/xrpc-server/tests/errors.test.ts @@ -0,0 +1,74 @@ +import * as http from 'http' +import { createServer, closeServer } from './_util' +import * as xrpcServer from '../src' +import xrpc, { XRPCError } from '@adxp/xrpc' + +const SCHEMAS = [ + { + lexicon: 1, + id: 'io.example.error', + type: 'query', + parameters: { + which: { type: 'string', default: 'foo' }, + }, + errors: [{ name: 'Foo' }, { name: 'Bar' }], + }, +] + +describe('Procedures', () => { + let s: http.Server + const server = xrpcServer.createServer(SCHEMAS) + server.method('io.example.error', (params: xrpcServer.Params) => { + if (params.which === 'foo') { + throw new xrpcServer.InvalidRequestError('It was this one!', 'Foo') + } else if (params.which === 'bar') { + return { status: 400, error: 'Bar', message: 'It was that one!' } + } else { + return { status: 400 } + } + }) + const client = xrpc.service(`http://localhost:8893`) + xrpc.addSchemas(SCHEMAS) + beforeAll(async () => { + s = await createServer(8893, server) + }) + afterAll(async () => { + await closeServer(s) + }) + + it('serves requests', async () => { + try { + await client.call('io.example.error', { + which: 'foo', + }) + throw new Error('Didnt throw') + } catch (e: any) { + expect(e instanceof XRPCError).toBeTruthy() + expect(e.success).toBeFalsy() + expect(e.error).toBe('Foo') + expect(e.message).toBe('It was this one!') + } + try { + await client.call('io.example.error', { + which: 'bar', + }) + throw new Error('Didnt throw') + } catch (e: any) { + expect(e instanceof XRPCError).toBeTruthy() + expect(e.success).toBeFalsy() + expect(e.error).toBe('Bar') + expect(e.message).toBe('It was that one!') + } + try { + await client.call('io.example.error', { + which: 'other', + }) + throw new Error('Didnt throw') + } catch (e: any) { + expect(e instanceof XRPCError).toBeTruthy() + expect(e.success).toBeFalsy() + expect(e.error).toBe('InvalidRequest') + expect(e.message).toBe('Invalid Request') + } + }) +}) diff --git a/packages/xrpc-server/tests/procedures.test.ts b/packages/xrpc-server/tests/procedures.test.ts index c5b242a5ccc..941079d4b38 100644 --- a/packages/xrpc-server/tests/procedures.test.ts +++ b/packages/xrpc-server/tests/procedures.test.ts @@ -99,7 +99,6 @@ describe('Procedures', () => { message: 'hello world', }) expect(res1.success).toBeTruthy() - expect(res1.error).toBeFalsy() expect(res1.headers['content-type']).toBe('text/plain; charset=utf-8') expect(res1.data).toBe('hello world') @@ -107,7 +106,6 @@ describe('Procedures', () => { encoding: 'text/plain', }) expect(res2.success).toBeTruthy() - expect(res2.error).toBeFalsy() expect(res2.headers['content-type']).toBe('text/plain; charset=utf-8') expect(res2.data).toBe('hello world') @@ -118,7 +116,6 @@ describe('Procedures', () => { { encoding: 'application/octet-stream' }, ) expect(res3.success).toBeTruthy() - expect(res3.error).toBeFalsy() expect(res3.headers['content-type']).toBe('application/octet-stream') expect(new TextDecoder().decode(res3.data)).toBe('hello world') @@ -128,7 +125,6 @@ describe('Procedures', () => { { message: 'hello world' }, ) expect(res4.success).toBeTruthy() - expect(res4.error).toBeFalsy() expect(res4.headers['content-type']).toBe('application/json; charset=utf-8') expect(res4.data?.message).toBe('hello world') }) diff --git a/packages/xrpc-server/tests/queries.test.ts b/packages/xrpc-server/tests/queries.test.ts index 8cb570c87ca..792e92eb730 100644 --- a/packages/xrpc-server/tests/queries.test.ts +++ b/packages/xrpc-server/tests/queries.test.ts @@ -67,7 +67,6 @@ describe('Queries', () => { message: 'hello world', }) expect(res1.success).toBeTruthy() - expect(res1.error).toBeFalsy() expect(res1.headers['content-type']).toBe('text/plain; charset=utf-8') expect(res1.data).toBe('hello world') @@ -75,7 +74,6 @@ describe('Queries', () => { message: 'hello world', }) expect(res2.success).toBeTruthy() - expect(res2.error).toBeFalsy() expect(res2.headers['content-type']).toBe('application/octet-stream') expect(new TextDecoder().decode(res2.data)).toBe('hello world') @@ -83,7 +81,6 @@ describe('Queries', () => { message: 'hello world', }) expect(res3.success).toBeTruthy() - expect(res3.error).toBeFalsy() expect(res3.headers['content-type']).toBe('application/json; charset=utf-8') expect(res3.data?.message).toBe('hello world') }) diff --git a/packages/xrpc/src/client.ts b/packages/xrpc/src/client.ts index 76ca5041b3d..12a3b43f1b4 100644 --- a/packages/xrpc/src/client.ts +++ b/packages/xrpc/src/client.ts @@ -113,7 +113,7 @@ export class ServiceClient { return new XRPCResponse(res.body, res.headers) } else { if (res.body && isErrorResponseBody(res.body)) { - throw new XRPCError(resCode, res.body.message) + throw new XRPCError(resCode, res.body.error, res.body.message) } else { throw new XRPCError(resCode) } diff --git a/packages/xrpc/src/types.ts b/packages/xrpc/src/types.ts index ab4a85abca3..0072ae9f1ae 100644 --- a/packages/xrpc/src/types.ts +++ b/packages/xrpc/src/types.ts @@ -22,6 +22,7 @@ export type FetchHandler = ( ) => Promise export const errorResponseBody = z.object({ + error: z.string().optional(), message: z.string().optional(), }) export type ErrorResponseBody = z.infer @@ -43,6 +44,22 @@ export enum ResponseType { UpstreamTimeout = 504, } +export const ResponseTypeNames = { + [ResponseType.InvalidResponse]: 'InvalidResponse', + [ResponseType.Success]: 'Success', + [ResponseType.InvalidRequest]: 'InvalidRequest', + [ResponseType.AuthRequired]: 'AuthenticationRequired', + [ResponseType.Forbidden]: 'Forbidden', + [ResponseType.XRPCNotSupported]: 'XRPCNotSupported', + [ResponseType.PayloadTooLarge]: 'PayloadTooLarge', + [ResponseType.RateLimitExceeded]: 'RateLimitExceeded', + [ResponseType.InternalServerError]: 'InternalServerError', + [ResponseType.MethodNotImplemented]: 'MethodNotImplemented', + [ResponseType.UpstreamFailure]: 'UpstreamFailure', + [ResponseType.NotEnoughResouces]: 'NotEnoughResouces', + [ResponseType.UpstreamTimeout]: 'UpstreamTimeout', +} + export const ResponseTypeStrings = { [ResponseType.InvalidResponse]: 'Invalid Response', [ResponseType.Success]: 'Success', @@ -61,20 +78,21 @@ export const ResponseTypeStrings = { export class XRPCResponse { success = true - error = false constructor(public data: any, public headers: Headers) {} } export class XRPCError extends Error { success = false - error = true - constructor(public code: ResponseType, message?: string) { - super( - message - ? `${ResponseTypeStrings[code]}: ${message}` - : ResponseTypeStrings[code], - ) + constructor( + public status: ResponseType, + public error?: string, + message?: string, + ) { + super(message || error || ResponseTypeStrings[status]) + if (!this.error) { + this.error = ResponseTypeNames[status] + } } } diff --git a/schemas/todo.adx/createAccount.json b/schemas/todo.adx/createAccount.json index 0c27e135e12..a37c909e770 100644 --- a/schemas/todo.adx/createAccount.json +++ b/schemas/todo.adx/createAccount.json @@ -25,5 +25,10 @@ "jwt": { "type": "string" } } } - } + }, + "errors": [ + {"name": "InvalidUsername"}, + {"name": "InvalidPassword"}, + {"name": "UsernameNotAvailable"} + ] } \ No newline at end of file