@atproto/xrpc-server 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -0
- package/babel.config.js +1 -0
- package/build.js +22 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +34790 -0
- package/dist/index.js.map +7 -0
- package/dist/logger.d.ts +2 -0
- package/dist/server.d.ts +19 -0
- package/dist/types.d.ts +115 -0
- package/dist/util.d.ts +10 -0
- package/jest.config.js +6 -0
- package/package.json +35 -0
- package/src/index.ts +2 -0
- package/src/logger.ts +5 -0
- package/src/server.ts +253 -0
- package/src/types.ts +159 -0
- package/src/util.ts +237 -0
- package/tests/_util.ts +20 -0
- package/tests/auth.test.ts +155 -0
- package/tests/bodies.test.ts +240 -0
- package/tests/errors.test.ts +214 -0
- package/tests/parameters.test.ts +189 -0
- package/tests/procedures.test.ts +165 -0
- package/tests/queries.test.ts +117 -0
- package/tsconfig.build.json +4 -0
- package/tsconfig.build.tsbuildinfo +1 -0
- package/tsconfig.json +14 -0
- package/update-pkg.js +14 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import * as http from 'http'
|
|
2
|
+
import { createServer, closeServer } from './_util'
|
|
3
|
+
import * as xrpcServer from '../src'
|
|
4
|
+
import xrpc, {
|
|
5
|
+
Client,
|
|
6
|
+
XRPCError,
|
|
7
|
+
XRPCInvalidResponseError,
|
|
8
|
+
} from '@atproto/xrpc'
|
|
9
|
+
|
|
10
|
+
const LEXICONS = [
|
|
11
|
+
{
|
|
12
|
+
lexicon: 1,
|
|
13
|
+
id: 'io.example.error',
|
|
14
|
+
defs: {
|
|
15
|
+
main: {
|
|
16
|
+
type: 'query',
|
|
17
|
+
parameters: {
|
|
18
|
+
type: 'params',
|
|
19
|
+
properties: {
|
|
20
|
+
which: { type: 'string', default: 'foo' },
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
errors: [{ name: 'Foo' }, { name: 'Bar' }],
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
lexicon: 1,
|
|
29
|
+
id: 'io.example.query',
|
|
30
|
+
defs: {
|
|
31
|
+
main: {
|
|
32
|
+
type: 'query',
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
lexicon: 1,
|
|
38
|
+
id: 'io.example.procedure',
|
|
39
|
+
defs: {
|
|
40
|
+
main: {
|
|
41
|
+
type: 'procedure',
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
lexicon: 1,
|
|
47
|
+
id: 'io.example.invalidResponse',
|
|
48
|
+
defs: {
|
|
49
|
+
main: {
|
|
50
|
+
type: 'query',
|
|
51
|
+
output: {
|
|
52
|
+
encoding: 'application/json',
|
|
53
|
+
schema: {
|
|
54
|
+
type: 'object',
|
|
55
|
+
required: ['expectedValue'],
|
|
56
|
+
properties: {
|
|
57
|
+
expectedValue: { type: 'string' },
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
const MISMATCHED_LEXICONS = [
|
|
67
|
+
{
|
|
68
|
+
lexicon: 1,
|
|
69
|
+
id: 'io.example.query',
|
|
70
|
+
defs: {
|
|
71
|
+
main: {
|
|
72
|
+
type: 'procedure',
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
lexicon: 1,
|
|
78
|
+
id: 'io.example.procedure',
|
|
79
|
+
defs: {
|
|
80
|
+
main: {
|
|
81
|
+
type: 'query',
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
lexicon: 1,
|
|
87
|
+
id: 'io.example.doesNotExist',
|
|
88
|
+
defs: {
|
|
89
|
+
main: {
|
|
90
|
+
type: 'query',
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
describe('Errors', () => {
|
|
97
|
+
let s: http.Server
|
|
98
|
+
const server = xrpcServer.createServer(LEXICONS, { validateResponse: false }) // disable validateResponse to test client validation
|
|
99
|
+
server.method('io.example.error', (ctx: { params: xrpcServer.Params }) => {
|
|
100
|
+
if (ctx.params.which === 'foo') {
|
|
101
|
+
throw new xrpcServer.InvalidRequestError('It was this one!', 'Foo')
|
|
102
|
+
} else if (ctx.params.which === 'bar') {
|
|
103
|
+
return { status: 400, error: 'Bar', message: 'It was that one!' }
|
|
104
|
+
} else {
|
|
105
|
+
return { status: 400 }
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
server.method('io.example.query', () => {
|
|
109
|
+
return undefined
|
|
110
|
+
})
|
|
111
|
+
// @ts-ignore We're intentionally giving the wrong response! -prf
|
|
112
|
+
server.method('io.example.invalidResponse', () => {
|
|
113
|
+
return { encoding: 'json', body: { something: 'else' } }
|
|
114
|
+
})
|
|
115
|
+
server.method('io.example.procedure', () => {
|
|
116
|
+
return undefined
|
|
117
|
+
})
|
|
118
|
+
const client = xrpc.service(`http://localhost:8893`)
|
|
119
|
+
xrpc.addLexicons(LEXICONS)
|
|
120
|
+
const badXrpc = new Client()
|
|
121
|
+
const badClient = badXrpc.service(`http://localhost:8893`)
|
|
122
|
+
badXrpc.addLexicons(MISMATCHED_LEXICONS)
|
|
123
|
+
beforeAll(async () => {
|
|
124
|
+
s = await createServer(8893, server)
|
|
125
|
+
})
|
|
126
|
+
afterAll(async () => {
|
|
127
|
+
await closeServer(s)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('serves requests', async () => {
|
|
131
|
+
try {
|
|
132
|
+
await client.call('io.example.error', {
|
|
133
|
+
which: 'foo',
|
|
134
|
+
})
|
|
135
|
+
throw new Error('Didnt throw')
|
|
136
|
+
} catch (e) {
|
|
137
|
+
expect(e instanceof XRPCError).toBeTruthy()
|
|
138
|
+
expect((e as XRPCError).success).toBeFalsy()
|
|
139
|
+
expect((e as XRPCError).error).toBe('Foo')
|
|
140
|
+
expect((e as XRPCError).message).toBe('It was this one!')
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
await client.call('io.example.error', {
|
|
144
|
+
which: 'bar',
|
|
145
|
+
})
|
|
146
|
+
throw new Error('Didnt throw')
|
|
147
|
+
} catch (e) {
|
|
148
|
+
expect(e instanceof XRPCError).toBeTruthy()
|
|
149
|
+
expect((e as XRPCError).success).toBeFalsy()
|
|
150
|
+
expect((e as XRPCError).error).toBe('Bar')
|
|
151
|
+
expect((e as XRPCError).message).toBe('It was that one!')
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
await client.call('io.example.error', {
|
|
155
|
+
which: 'other',
|
|
156
|
+
})
|
|
157
|
+
throw new Error('Didnt throw')
|
|
158
|
+
} catch (e) {
|
|
159
|
+
expect(e instanceof XRPCError).toBeTruthy()
|
|
160
|
+
expect((e as XRPCError).success).toBeFalsy()
|
|
161
|
+
expect((e as XRPCError).error).toBe('InvalidRequest')
|
|
162
|
+
expect((e as XRPCError).message).toBe('Invalid Request')
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
await client.call('io.example.invalidResponse')
|
|
166
|
+
throw new Error('Didnt throw')
|
|
167
|
+
} catch (e: any) {
|
|
168
|
+
expect(e instanceof XRPCError).toBeTruthy()
|
|
169
|
+
expect(e instanceof XRPCInvalidResponseError).toBeTruthy()
|
|
170
|
+
expect(e.success).toBeFalsy()
|
|
171
|
+
expect(e.error).toBe('Invalid Response')
|
|
172
|
+
expect(e.message).toBe(
|
|
173
|
+
'The server gave an invalid response and may be out of date.',
|
|
174
|
+
)
|
|
175
|
+
const err = e as XRPCInvalidResponseError
|
|
176
|
+
expect(err.validationError.message).toBe(
|
|
177
|
+
'Output must have the property "expectedValue"',
|
|
178
|
+
)
|
|
179
|
+
expect(err.responseBody).toStrictEqual({ something: 'else' })
|
|
180
|
+
}
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
it('serves error for missing/mismatch schemas', async () => {
|
|
184
|
+
await client.call('io.example.query') // No error
|
|
185
|
+
await client.call('io.example.procedure') // No error
|
|
186
|
+
try {
|
|
187
|
+
await badClient.call('io.example.query')
|
|
188
|
+
throw new Error('Didnt throw')
|
|
189
|
+
} catch (e: any) {
|
|
190
|
+
expect(e instanceof XRPCError).toBeTruthy()
|
|
191
|
+
expect(e.success).toBeFalsy()
|
|
192
|
+
expect(e.error).toBe('InvalidRequest')
|
|
193
|
+
expect(e.message).toBe('Incorrect HTTP method (POST) expected GET')
|
|
194
|
+
}
|
|
195
|
+
try {
|
|
196
|
+
await badClient.call('io.example.procedure')
|
|
197
|
+
throw new Error('Didnt throw')
|
|
198
|
+
} catch (e: any) {
|
|
199
|
+
expect(e instanceof XRPCError).toBeTruthy()
|
|
200
|
+
expect(e.success).toBeFalsy()
|
|
201
|
+
expect(e.error).toBe('InvalidRequest')
|
|
202
|
+
expect(e.message).toBe('Incorrect HTTP method (GET) expected POST')
|
|
203
|
+
}
|
|
204
|
+
try {
|
|
205
|
+
await badClient.call('io.example.doesNotExist')
|
|
206
|
+
throw new Error('Didnt throw')
|
|
207
|
+
} catch (e: any) {
|
|
208
|
+
expect(e instanceof XRPCError).toBeTruthy()
|
|
209
|
+
expect(e.success).toBeFalsy()
|
|
210
|
+
expect(e.error).toBe('MethodNotImplemented')
|
|
211
|
+
expect(e.message).toBe('Method Not Implemented')
|
|
212
|
+
}
|
|
213
|
+
})
|
|
214
|
+
})
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import * as http from 'http'
|
|
2
|
+
import { createServer, closeServer } from './_util'
|
|
3
|
+
import * as xrpcServer from '../src'
|
|
4
|
+
import xrpc from '@atproto/xrpc'
|
|
5
|
+
|
|
6
|
+
const LEXICONS = [
|
|
7
|
+
{
|
|
8
|
+
lexicon: 1,
|
|
9
|
+
id: 'io.example.paramTest',
|
|
10
|
+
defs: {
|
|
11
|
+
main: {
|
|
12
|
+
type: 'query',
|
|
13
|
+
parameters: {
|
|
14
|
+
type: 'params',
|
|
15
|
+
required: ['str', 'int', 'num', 'bool', 'arr'],
|
|
16
|
+
properties: {
|
|
17
|
+
str: { type: 'string', minLength: 2, maxLength: 10 },
|
|
18
|
+
int: { type: 'integer', minimum: 2, maximum: 10 },
|
|
19
|
+
num: { type: 'number', minimum: 2, maximum: 10 },
|
|
20
|
+
bool: { type: 'boolean' },
|
|
21
|
+
arr: { type: 'array', items: { type: 'integer' }, maxLength: 2 },
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
output: {
|
|
25
|
+
encoding: 'application/json',
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
describe('Parameters', () => {
|
|
33
|
+
let s: http.Server
|
|
34
|
+
const server = xrpcServer.createServer(LEXICONS)
|
|
35
|
+
server.method(
|
|
36
|
+
'io.example.paramTest',
|
|
37
|
+
(ctx: { params: xrpcServer.Params }) => ({
|
|
38
|
+
encoding: 'json',
|
|
39
|
+
body: ctx.params,
|
|
40
|
+
}),
|
|
41
|
+
)
|
|
42
|
+
const client = xrpc.service(`http://localhost:8889`)
|
|
43
|
+
xrpc.addLexicons(LEXICONS)
|
|
44
|
+
beforeAll(async () => {
|
|
45
|
+
s = await createServer(8889, server)
|
|
46
|
+
})
|
|
47
|
+
afterAll(async () => {
|
|
48
|
+
await closeServer(s)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('validates query params', async () => {
|
|
52
|
+
const res1 = await client.call('io.example.paramTest', {
|
|
53
|
+
str: 'valid',
|
|
54
|
+
int: 5,
|
|
55
|
+
num: 5.5,
|
|
56
|
+
bool: true,
|
|
57
|
+
arr: [1, 2],
|
|
58
|
+
})
|
|
59
|
+
expect(res1.success).toBeTruthy()
|
|
60
|
+
expect(res1.data.str).toBe('valid')
|
|
61
|
+
expect(res1.data.int).toBe(5)
|
|
62
|
+
expect(res1.data.num).toBe(5.5)
|
|
63
|
+
expect(res1.data.bool).toBe(true)
|
|
64
|
+
expect(res1.data.arr).toEqual([1, 2])
|
|
65
|
+
|
|
66
|
+
const res2 = await client.call('io.example.paramTest', {
|
|
67
|
+
str: 10,
|
|
68
|
+
int: '5',
|
|
69
|
+
num: '5.5',
|
|
70
|
+
bool: 'foo',
|
|
71
|
+
arr: '3',
|
|
72
|
+
})
|
|
73
|
+
expect(res2.success).toBeTruthy()
|
|
74
|
+
expect(res2.data.str).toBe('10')
|
|
75
|
+
expect(res2.data.int).toBe(5)
|
|
76
|
+
expect(res2.data.num).toBe(5.5)
|
|
77
|
+
expect(res2.data.bool).toBe(true)
|
|
78
|
+
expect(res2.data.arr).toEqual([3])
|
|
79
|
+
|
|
80
|
+
await expect(
|
|
81
|
+
client.call('io.example.paramTest', {
|
|
82
|
+
str: 'n',
|
|
83
|
+
int: 5,
|
|
84
|
+
num: 5.5,
|
|
85
|
+
bool: true,
|
|
86
|
+
arr: [1],
|
|
87
|
+
}),
|
|
88
|
+
).rejects.toThrow('str must not be shorter than 2 characters')
|
|
89
|
+
await expect(
|
|
90
|
+
client.call('io.example.paramTest', {
|
|
91
|
+
str: 'loooooooooooooong',
|
|
92
|
+
int: 5,
|
|
93
|
+
num: 5.5,
|
|
94
|
+
bool: true,
|
|
95
|
+
arr: [1],
|
|
96
|
+
}),
|
|
97
|
+
).rejects.toThrow('str must not be longer than 10 characters')
|
|
98
|
+
await expect(
|
|
99
|
+
client.call('io.example.paramTest', {
|
|
100
|
+
int: 5,
|
|
101
|
+
num: 5.5,
|
|
102
|
+
bool: true,
|
|
103
|
+
arr: [1],
|
|
104
|
+
}),
|
|
105
|
+
).rejects.toThrow(`Params must have the property "str"`)
|
|
106
|
+
|
|
107
|
+
await expect(
|
|
108
|
+
client.call('io.example.paramTest', {
|
|
109
|
+
str: 'valid',
|
|
110
|
+
int: -1,
|
|
111
|
+
num: 5.5,
|
|
112
|
+
bool: true,
|
|
113
|
+
arr: [1],
|
|
114
|
+
}),
|
|
115
|
+
).rejects.toThrow('int can not be less than 2')
|
|
116
|
+
await expect(
|
|
117
|
+
client.call('io.example.paramTest', {
|
|
118
|
+
str: 'valid',
|
|
119
|
+
int: 11,
|
|
120
|
+
num: 5.5,
|
|
121
|
+
bool: true,
|
|
122
|
+
arr: [1],
|
|
123
|
+
}),
|
|
124
|
+
).rejects.toThrow('int can not be greater than 10')
|
|
125
|
+
await expect(
|
|
126
|
+
client.call('io.example.paramTest', {
|
|
127
|
+
str: 'valid',
|
|
128
|
+
num: 5.5,
|
|
129
|
+
bool: true,
|
|
130
|
+
arr: [1],
|
|
131
|
+
}),
|
|
132
|
+
).rejects.toThrow(`Params must have the property "int"`)
|
|
133
|
+
|
|
134
|
+
await expect(
|
|
135
|
+
client.call('io.example.paramTest', {
|
|
136
|
+
str: 'valid',
|
|
137
|
+
int: 5,
|
|
138
|
+
num: -5.5,
|
|
139
|
+
bool: true,
|
|
140
|
+
arr: [1],
|
|
141
|
+
}),
|
|
142
|
+
).rejects.toThrow('num can not be less than 2')
|
|
143
|
+
await expect(
|
|
144
|
+
client.call('io.example.paramTest', {
|
|
145
|
+
str: 'valid',
|
|
146
|
+
int: 5,
|
|
147
|
+
num: 50.5,
|
|
148
|
+
bool: true,
|
|
149
|
+
arr: [1],
|
|
150
|
+
}),
|
|
151
|
+
).rejects.toThrow('num can not be greater than 10')
|
|
152
|
+
await expect(
|
|
153
|
+
client.call('io.example.paramTest', {
|
|
154
|
+
str: 'valid',
|
|
155
|
+
int: 5,
|
|
156
|
+
bool: true,
|
|
157
|
+
arr: [1],
|
|
158
|
+
}),
|
|
159
|
+
).rejects.toThrow(`Params must have the property "num"`)
|
|
160
|
+
|
|
161
|
+
await expect(
|
|
162
|
+
client.call('io.example.paramTest', {
|
|
163
|
+
str: 'valid',
|
|
164
|
+
int: 5,
|
|
165
|
+
num: 5.5,
|
|
166
|
+
arr: [1],
|
|
167
|
+
}),
|
|
168
|
+
).rejects.toThrow(`Params must have the property "bool"`)
|
|
169
|
+
|
|
170
|
+
await expect(
|
|
171
|
+
client.call('io.example.paramTest', {
|
|
172
|
+
str: 'valid',
|
|
173
|
+
int: 5,
|
|
174
|
+
num: 5.5,
|
|
175
|
+
bool: true,
|
|
176
|
+
arr: [],
|
|
177
|
+
}),
|
|
178
|
+
).rejects.toThrow('Error: Params must have the property "arr"')
|
|
179
|
+
await expect(
|
|
180
|
+
client.call('io.example.paramTest', {
|
|
181
|
+
str: 'valid',
|
|
182
|
+
int: 5,
|
|
183
|
+
num: 5.5,
|
|
184
|
+
bool: true,
|
|
185
|
+
arr: [1, 2, 3],
|
|
186
|
+
}),
|
|
187
|
+
).rejects.toThrow('Error: arr must not have more than 2 elements')
|
|
188
|
+
})
|
|
189
|
+
})
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import * as http from 'http'
|
|
2
|
+
import { Readable } from 'stream'
|
|
3
|
+
import xrpc from '@atproto/xrpc'
|
|
4
|
+
import { createServer, closeServer } from './_util'
|
|
5
|
+
import * as xrpcServer from '../src'
|
|
6
|
+
|
|
7
|
+
const LEXICONS = [
|
|
8
|
+
{
|
|
9
|
+
lexicon: 1,
|
|
10
|
+
id: 'io.example.ping1',
|
|
11
|
+
defs: {
|
|
12
|
+
main: {
|
|
13
|
+
type: 'procedure',
|
|
14
|
+
parameters: {
|
|
15
|
+
type: 'params',
|
|
16
|
+
properties: {
|
|
17
|
+
message: { type: 'string' },
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
output: {
|
|
21
|
+
encoding: 'text/plain',
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
lexicon: 1,
|
|
28
|
+
id: 'io.example.ping2',
|
|
29
|
+
defs: {
|
|
30
|
+
main: {
|
|
31
|
+
type: 'procedure',
|
|
32
|
+
input: {
|
|
33
|
+
encoding: 'text/plain',
|
|
34
|
+
},
|
|
35
|
+
output: {
|
|
36
|
+
encoding: 'text/plain',
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
lexicon: 1,
|
|
43
|
+
id: 'io.example.ping3',
|
|
44
|
+
defs: {
|
|
45
|
+
main: {
|
|
46
|
+
type: 'procedure',
|
|
47
|
+
input: {
|
|
48
|
+
encoding: 'application/octet-stream',
|
|
49
|
+
},
|
|
50
|
+
output: {
|
|
51
|
+
encoding: 'application/octet-stream',
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
lexicon: 1,
|
|
58
|
+
id: 'io.example.ping4',
|
|
59
|
+
defs: {
|
|
60
|
+
main: {
|
|
61
|
+
type: 'procedure',
|
|
62
|
+
input: {
|
|
63
|
+
encoding: 'application/json',
|
|
64
|
+
schema: {
|
|
65
|
+
type: 'object',
|
|
66
|
+
required: ['message'],
|
|
67
|
+
properties: { message: { type: 'string' } },
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
output: {
|
|
71
|
+
encoding: 'application/json',
|
|
72
|
+
schema: {
|
|
73
|
+
type: 'object',
|
|
74
|
+
required: ['message'],
|
|
75
|
+
properties: { message: { type: 'string' } },
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
describe('Procedures', () => {
|
|
84
|
+
let s: http.Server
|
|
85
|
+
const server = xrpcServer.createServer(LEXICONS)
|
|
86
|
+
server.method('io.example.ping1', (ctx: { params: xrpcServer.Params }) => {
|
|
87
|
+
return { encoding: 'text/plain', body: ctx.params.message }
|
|
88
|
+
})
|
|
89
|
+
server.method(
|
|
90
|
+
'io.example.ping2',
|
|
91
|
+
(ctx: { params: xrpcServer.Params; input?: xrpcServer.HandlerInput }) => {
|
|
92
|
+
return { encoding: 'text/plain', body: ctx.input?.body }
|
|
93
|
+
},
|
|
94
|
+
)
|
|
95
|
+
server.method(
|
|
96
|
+
'io.example.ping3',
|
|
97
|
+
async (ctx: {
|
|
98
|
+
params: xrpcServer.Params
|
|
99
|
+
input?: xrpcServer.HandlerInput
|
|
100
|
+
}) => {
|
|
101
|
+
if (!(ctx.input?.body instanceof Readable))
|
|
102
|
+
throw new Error('Input not readable')
|
|
103
|
+
const buffers: Buffer[] = []
|
|
104
|
+
for await (const data of ctx.input.body) {
|
|
105
|
+
buffers.push(data)
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
encoding: 'application/octet-stream',
|
|
109
|
+
body: Buffer.concat(buffers),
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
)
|
|
113
|
+
server.method(
|
|
114
|
+
'io.example.ping4',
|
|
115
|
+
(ctx: { params: xrpcServer.Params; input?: xrpcServer.HandlerInput }) => {
|
|
116
|
+
return {
|
|
117
|
+
encoding: 'application/json',
|
|
118
|
+
body: { message: ctx.input?.body?.message },
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
)
|
|
122
|
+
const client = xrpc.service(`http://localhost:8891`)
|
|
123
|
+
xrpc.addLexicons(LEXICONS)
|
|
124
|
+
beforeAll(async () => {
|
|
125
|
+
s = await createServer(8891, server)
|
|
126
|
+
})
|
|
127
|
+
afterAll(async () => {
|
|
128
|
+
await closeServer(s)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('serves requests', async () => {
|
|
132
|
+
const res1 = await client.call('io.example.ping1', {
|
|
133
|
+
message: 'hello world',
|
|
134
|
+
})
|
|
135
|
+
expect(res1.success).toBeTruthy()
|
|
136
|
+
expect(res1.headers['content-type']).toBe('text/plain; charset=utf-8')
|
|
137
|
+
expect(res1.data).toBe('hello world')
|
|
138
|
+
|
|
139
|
+
const res2 = await client.call('io.example.ping2', {}, 'hello world', {
|
|
140
|
+
encoding: 'text/plain',
|
|
141
|
+
})
|
|
142
|
+
expect(res2.success).toBeTruthy()
|
|
143
|
+
expect(res2.headers['content-type']).toBe('text/plain; charset=utf-8')
|
|
144
|
+
expect(res2.data).toBe('hello world')
|
|
145
|
+
|
|
146
|
+
const res3 = await client.call(
|
|
147
|
+
'io.example.ping3',
|
|
148
|
+
{},
|
|
149
|
+
new TextEncoder().encode('hello world'),
|
|
150
|
+
{ encoding: 'application/octet-stream' },
|
|
151
|
+
)
|
|
152
|
+
expect(res3.success).toBeTruthy()
|
|
153
|
+
expect(res3.headers['content-type']).toBe('application/octet-stream')
|
|
154
|
+
expect(new TextDecoder().decode(res3.data)).toBe('hello world')
|
|
155
|
+
|
|
156
|
+
const res4 = await client.call(
|
|
157
|
+
'io.example.ping4',
|
|
158
|
+
{},
|
|
159
|
+
{ message: 'hello world' },
|
|
160
|
+
)
|
|
161
|
+
expect(res4.success).toBeTruthy()
|
|
162
|
+
expect(res4.headers['content-type']).toBe('application/json; charset=utf-8')
|
|
163
|
+
expect(res4.data?.message).toBe('hello world')
|
|
164
|
+
})
|
|
165
|
+
})
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import * as http from 'http'
|
|
2
|
+
import { createServer, closeServer } from './_util'
|
|
3
|
+
import * as xrpcServer from '../src'
|
|
4
|
+
import xrpc from '@atproto/xrpc'
|
|
5
|
+
|
|
6
|
+
const LEXICONS = [
|
|
7
|
+
{
|
|
8
|
+
lexicon: 1,
|
|
9
|
+
id: 'io.example.ping1',
|
|
10
|
+
defs: {
|
|
11
|
+
main: {
|
|
12
|
+
type: 'query',
|
|
13
|
+
parameters: {
|
|
14
|
+
type: 'params',
|
|
15
|
+
properties: {
|
|
16
|
+
message: { type: 'string' },
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
output: {
|
|
20
|
+
encoding: 'text/plain',
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
lexicon: 1,
|
|
27
|
+
id: 'io.example.ping2',
|
|
28
|
+
defs: {
|
|
29
|
+
main: {
|
|
30
|
+
type: 'query',
|
|
31
|
+
parameters: {
|
|
32
|
+
type: 'params',
|
|
33
|
+
properties: {
|
|
34
|
+
message: { type: 'string' },
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
output: {
|
|
38
|
+
encoding: 'application/octet-stream',
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
lexicon: 1,
|
|
45
|
+
id: 'io.example.ping3',
|
|
46
|
+
defs: {
|
|
47
|
+
main: {
|
|
48
|
+
type: 'query',
|
|
49
|
+
parameters: {
|
|
50
|
+
type: 'params',
|
|
51
|
+
properties: {
|
|
52
|
+
message: { type: 'string' },
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
output: {
|
|
56
|
+
encoding: 'application/json',
|
|
57
|
+
schema: {
|
|
58
|
+
type: 'object',
|
|
59
|
+
required: ['message'],
|
|
60
|
+
properties: { message: { type: 'string' } },
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
describe('Queries', () => {
|
|
69
|
+
let s: http.Server
|
|
70
|
+
const server = xrpcServer.createServer(LEXICONS)
|
|
71
|
+
server.method('io.example.ping1', (ctx: { params: xrpcServer.Params }) => {
|
|
72
|
+
return { encoding: 'text/plain', body: ctx.params.message }
|
|
73
|
+
})
|
|
74
|
+
server.method('io.example.ping2', (ctx: { params: xrpcServer.Params }) => {
|
|
75
|
+
return {
|
|
76
|
+
encoding: 'application/octet-stream',
|
|
77
|
+
body: new TextEncoder().encode(String(ctx.params.message)),
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
server.method('io.example.ping3', (ctx: { params: xrpcServer.Params }) => {
|
|
81
|
+
return {
|
|
82
|
+
encoding: 'application/json',
|
|
83
|
+
body: { message: ctx.params.message },
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
const client = xrpc.service(`http://localhost:8890`)
|
|
87
|
+
xrpc.addLexicons(LEXICONS)
|
|
88
|
+
beforeAll(async () => {
|
|
89
|
+
s = await createServer(8890, server)
|
|
90
|
+
})
|
|
91
|
+
afterAll(async () => {
|
|
92
|
+
await closeServer(s)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('serves requests', async () => {
|
|
96
|
+
const res1 = await client.call('io.example.ping1', {
|
|
97
|
+
message: 'hello world',
|
|
98
|
+
})
|
|
99
|
+
expect(res1.success).toBeTruthy()
|
|
100
|
+
expect(res1.headers['content-type']).toBe('text/plain; charset=utf-8')
|
|
101
|
+
expect(res1.data).toBe('hello world')
|
|
102
|
+
|
|
103
|
+
const res2 = await client.call('io.example.ping2', {
|
|
104
|
+
message: 'hello world',
|
|
105
|
+
})
|
|
106
|
+
expect(res2.success).toBeTruthy()
|
|
107
|
+
expect(res2.headers['content-type']).toBe('application/octet-stream')
|
|
108
|
+
expect(new TextDecoder().decode(res2.data)).toBe('hello world')
|
|
109
|
+
|
|
110
|
+
const res3 = await client.call('io.example.ping3', {
|
|
111
|
+
message: 'hello world',
|
|
112
|
+
})
|
|
113
|
+
expect(res3.success).toBeTruthy()
|
|
114
|
+
expect(res3.headers['content-type']).toBe('application/json; charset=utf-8')
|
|
115
|
+
expect(res3.data?.message).toBe('hello world')
|
|
116
|
+
})
|
|
117
|
+
})
|