ts-procedures 1.0.0
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 +459 -0
- package/package.json +50 -0
- package/src/errors.ts +39 -0
- package/src/exports.ts +6 -0
- package/src/index.test.ts +364 -0
- package/src/index.ts +180 -0
- package/src/schema/compute-schema.test.ts +128 -0
- package/src/schema/compute-schema.ts +55 -0
- package/src/schema/extract-json-schema.test.ts +25 -0
- package/src/schema/extract-json-schema.ts +15 -0
- package/src/schema/parser.test.ts +156 -0
- package/src/schema/parser.ts +92 -0
- package/src/schema/resolve-schema-lib.test.ts +19 -0
- package/src/schema/resolve-schema-lib.ts +29 -0
- package/src/schema/types.ts +17 -0
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
2
|
+
import { describe, expect, test } from 'vitest'
|
|
3
|
+
import { Procedures } from './index.js'
|
|
4
|
+
import { v } from 'suretype'
|
|
5
|
+
import { Type } from 'typebox'
|
|
6
|
+
import {
|
|
7
|
+
ProcedureError,
|
|
8
|
+
ProcedureValidationError,
|
|
9
|
+
} from './errors.js'
|
|
10
|
+
|
|
11
|
+
describe('Procedures', () => {
|
|
12
|
+
test('Procedures', () => {
|
|
13
|
+
const result = Procedures({
|
|
14
|
+
onCreate: () => {
|
|
15
|
+
return undefined
|
|
16
|
+
},
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
expect(result).toHaveProperty('Create')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
test('Procedures generic context & extended config', () => {
|
|
23
|
+
interface CustomContext {
|
|
24
|
+
authToken: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface ExtendedConfig {
|
|
28
|
+
customProp: string
|
|
29
|
+
optionalProp?: number
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const { Create } = Procedures<CustomContext, ExtendedConfig>()
|
|
33
|
+
|
|
34
|
+
const {info} = Create(
|
|
35
|
+
'TestProcedure',
|
|
36
|
+
{
|
|
37
|
+
// should not throw type errors
|
|
38
|
+
customProp: 'customProp',
|
|
39
|
+
},
|
|
40
|
+
async (ctx) => {
|
|
41
|
+
// should not throw type errors
|
|
42
|
+
return ctx.authToken
|
|
43
|
+
},
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
expect(info.customProp).toEqual('customProp')
|
|
47
|
+
expect(info.optionalProp).toEqual(undefined)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test('Create Single Procedures', () => {
|
|
51
|
+
const { procedure: procedure1, info: info1 } = Procedures().Create(
|
|
52
|
+
'test1',
|
|
53
|
+
{},
|
|
54
|
+
async () => {
|
|
55
|
+
return '1'
|
|
56
|
+
},
|
|
57
|
+
)
|
|
58
|
+
const { procedure: procedure2, info: info2 } = Procedures().Create(
|
|
59
|
+
'test2',
|
|
60
|
+
{},
|
|
61
|
+
async () => {
|
|
62
|
+
return '2'
|
|
63
|
+
},
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
expect(procedure1).toBeDefined()
|
|
67
|
+
expect(info1).toBeDefined()
|
|
68
|
+
expect(procedure2).toBeDefined()
|
|
69
|
+
expect(info2).toBeDefined()
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
test('Procedures - Create call', () =>
|
|
73
|
+
new Promise<void>((done) => {
|
|
74
|
+
let mockHttpCall: any
|
|
75
|
+
|
|
76
|
+
const { Create } = Procedures({
|
|
77
|
+
onCreate: ({ handler }) => {
|
|
78
|
+
mockHttpCall = handler
|
|
79
|
+
},
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
Create(
|
|
83
|
+
'Handler',
|
|
84
|
+
{
|
|
85
|
+
schema: {
|
|
86
|
+
params: v.object({ name: v.string() }),
|
|
87
|
+
returnType: v.string(),
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
async (ctx, params) => {
|
|
91
|
+
expect(params).toEqual({ name: 'name' })
|
|
92
|
+
done()
|
|
93
|
+
return 'name'
|
|
94
|
+
},
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
mockHttpCall({}, { name: 'name' })
|
|
98
|
+
}))
|
|
99
|
+
|
|
100
|
+
test('Procedures - Create call w/ Typebox', () =>
|
|
101
|
+
new Promise<void>((done) => {
|
|
102
|
+
let mockHttpCall: any
|
|
103
|
+
|
|
104
|
+
const { Create } = Procedures({
|
|
105
|
+
onCreate: ({ handler, config, name }) => {
|
|
106
|
+
mockHttpCall = handler
|
|
107
|
+
},
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
Create(
|
|
111
|
+
'Handler',
|
|
112
|
+
{
|
|
113
|
+
schema: {
|
|
114
|
+
params: Type.Object({ name: Type.Optional(Type.String()) }),
|
|
115
|
+
returnType: Type.String(),
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
async (ctx, params) => {
|
|
119
|
+
expect(params).toEqual({ name: 'name' })
|
|
120
|
+
done()
|
|
121
|
+
return 'name'
|
|
122
|
+
},
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
mockHttpCall({}, { name: 'name' })
|
|
126
|
+
}))
|
|
127
|
+
|
|
128
|
+
test('Procedures - Create returns a handler to call/test the Procedure registration', async () => {
|
|
129
|
+
let ProcedureRegisteredCbHandler: any
|
|
130
|
+
|
|
131
|
+
const { Create } = Procedures({
|
|
132
|
+
onCreate: (Procedure) => {
|
|
133
|
+
ProcedureRegisteredCbHandler = Procedure.handler
|
|
134
|
+
},
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
const { NamedExportHandler, procedure, info } = Create(
|
|
138
|
+
'NamedExportHandler',
|
|
139
|
+
{
|
|
140
|
+
description: 'Handler description',
|
|
141
|
+
schema: {
|
|
142
|
+
params: v.object({ number: v.number() }),
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
async (ctx, params) => {
|
|
146
|
+
return params.number
|
|
147
|
+
},
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
expect(NamedExportHandler).toBeDefined()
|
|
151
|
+
expect(procedure).toBeDefined()
|
|
152
|
+
expect(ProcedureRegisteredCbHandler).toEqual(NamedExportHandler)
|
|
153
|
+
expect(ProcedureRegisteredCbHandler).toEqual(procedure)
|
|
154
|
+
|
|
155
|
+
const result = NamedExportHandler({}, { number: 1 })
|
|
156
|
+
|
|
157
|
+
expect(result).toBeDefined()
|
|
158
|
+
expect(result).toBeInstanceOf(Promise)
|
|
159
|
+
await expect(result).resolves.toEqual(1)
|
|
160
|
+
|
|
161
|
+
expect(info).toBeDefined()
|
|
162
|
+
expect(info).toBeInstanceOf(Object)
|
|
163
|
+
expect(info.schema).toHaveProperty('params')
|
|
164
|
+
expect(info.schema.params).toEqual({
|
|
165
|
+
type: 'object',
|
|
166
|
+
properties: { number: { type: 'number' } },
|
|
167
|
+
})
|
|
168
|
+
expect(info).toHaveProperty('description')
|
|
169
|
+
expect(info.description).toEqual('Handler description')
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
test('Procedures - Create params validation w/ no params provided', () =>
|
|
173
|
+
new Promise<void>((done) => {
|
|
174
|
+
let mockHttpCall: any
|
|
175
|
+
|
|
176
|
+
const { Create } = Procedures({
|
|
177
|
+
onCreate: ({ handler, config, name }) => {
|
|
178
|
+
mockHttpCall = (callParams: any) => {
|
|
179
|
+
if (config.validation?.params) {
|
|
180
|
+
const { errors } = config.validation.params(callParams)
|
|
181
|
+
|
|
182
|
+
if (errors && 'message' in errors[0]) {
|
|
183
|
+
expect(errors[0].message).toEqual('must be object')
|
|
184
|
+
done()
|
|
185
|
+
return
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
handler(callParams, {})
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
Create(
|
|
195
|
+
'test',
|
|
196
|
+
{
|
|
197
|
+
schema: {
|
|
198
|
+
params: v.object({}),
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
async () => {
|
|
202
|
+
done()
|
|
203
|
+
},
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
mockHttpCall()
|
|
207
|
+
}))
|
|
208
|
+
|
|
209
|
+
test('Procedures - Create params validation w/ missing params', async () =>
|
|
210
|
+
new Promise<void>((done) => {
|
|
211
|
+
let mockHttpCall: any
|
|
212
|
+
|
|
213
|
+
const { Create } = Procedures({
|
|
214
|
+
onCreate: async ({ handler, config, name }) => {
|
|
215
|
+
mockHttpCall = async (callParams: any) => {
|
|
216
|
+
if (config.validation?.params) {
|
|
217
|
+
const { errors } = config.validation.params(callParams)
|
|
218
|
+
expect(errors).toBeDefined()
|
|
219
|
+
expect(errors?.length).toEqual(2)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
await handler(callParams, {})
|
|
224
|
+
} catch (e: any) {
|
|
225
|
+
expect(e).instanceof(ProcedureValidationError)
|
|
226
|
+
expect(e.errors.length).toEqual(2)
|
|
227
|
+
done()
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
Create(
|
|
234
|
+
'test',
|
|
235
|
+
{
|
|
236
|
+
schema: {
|
|
237
|
+
params: v.object({
|
|
238
|
+
name: v.string().required(),
|
|
239
|
+
id: v.number().required(),
|
|
240
|
+
email: v.string(),
|
|
241
|
+
}),
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
async () => {
|
|
245
|
+
return
|
|
246
|
+
},
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
mockHttpCall({})
|
|
250
|
+
}))
|
|
251
|
+
|
|
252
|
+
test('Procedures - Create call provides ctx to handler', () =>
|
|
253
|
+
new Promise<void>((done) => {
|
|
254
|
+
let mockHttpCall: any
|
|
255
|
+
|
|
256
|
+
const { Create } = Procedures<{
|
|
257
|
+
testCtx: string
|
|
258
|
+
}>({
|
|
259
|
+
onCreate: ({ handler }) => {
|
|
260
|
+
mockHttpCall = () => handler({ testCtx: 'testCtx' })
|
|
261
|
+
},
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
Create('test', {}, async (ctx, params) => {
|
|
265
|
+
expect(ctx.testCtx).toEqual('testCtx')
|
|
266
|
+
done()
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
mockHttpCall()
|
|
270
|
+
}))
|
|
271
|
+
|
|
272
|
+
test('Procedure handler can throw local ctx error and is caught', async () => {
|
|
273
|
+
const { Create } = Procedures()
|
|
274
|
+
|
|
275
|
+
const { TestProcedureHandlerError } = Create(
|
|
276
|
+
'TestProcedureHandlerError',
|
|
277
|
+
{},
|
|
278
|
+
async (ctx) => {
|
|
279
|
+
throw ctx.error( 'Local context error')
|
|
280
|
+
},
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
try {
|
|
284
|
+
await TestProcedureHandlerError({}, {})
|
|
285
|
+
} catch (e: any) {
|
|
286
|
+
expect(e).toBeInstanceOf(ProcedureError)
|
|
287
|
+
|
|
288
|
+
expect(e.message).toEqual('Local context error')
|
|
289
|
+
expect(e.procedureName).toEqual('TestProcedureHandlerError')
|
|
290
|
+
}
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
test('Procedures - getRegisteredProcedures', () => {
|
|
294
|
+
const { Create, getProcedures } = Procedures({
|
|
295
|
+
onCreate: () => {
|
|
296
|
+
return undefined
|
|
297
|
+
},
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
Create(
|
|
301
|
+
'test-docs',
|
|
302
|
+
{
|
|
303
|
+
schema: {
|
|
304
|
+
params: v.object({ name: v.string().required() }),
|
|
305
|
+
returnType: v.string(),
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
async () => {
|
|
309
|
+
return 'test-docs'
|
|
310
|
+
},
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
expect(getProcedures().get('test-docs')).toBeDefined()
|
|
314
|
+
expect(getProcedures().get('test-docs')?.config?.schema).toEqual({
|
|
315
|
+
params: {
|
|
316
|
+
type: 'object',
|
|
317
|
+
properties: {
|
|
318
|
+
name: {
|
|
319
|
+
type: 'string',
|
|
320
|
+
},
|
|
321
|
+
},
|
|
322
|
+
required: ['name'],
|
|
323
|
+
},
|
|
324
|
+
returnType: {
|
|
325
|
+
type: 'string',
|
|
326
|
+
},
|
|
327
|
+
})
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
test('Procedures - context() throws', async () => {
|
|
331
|
+
interface CustomContext {
|
|
332
|
+
authToken: string
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const { Create } = Procedures<CustomContext>()
|
|
336
|
+
|
|
337
|
+
function validateAuthToken(token: string) {
|
|
338
|
+
return token === 'valid-token'
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const { CheckIsAuthenticated } = Create(
|
|
342
|
+
'CheckIsAuthenticated',
|
|
343
|
+
{
|
|
344
|
+
schema: {
|
|
345
|
+
returnType: v.string(),
|
|
346
|
+
},
|
|
347
|
+
},
|
|
348
|
+
async (ctx) => {
|
|
349
|
+
if (!validateAuthToken(ctx.authToken)) {
|
|
350
|
+
throw ctx.error('Invalid auth token')
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return 'User authentication is valid'
|
|
354
|
+
},
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
await expect(
|
|
358
|
+
CheckIsAuthenticated({ authToken: 'valid-token' }, {}),
|
|
359
|
+
).resolves.toEqual('User authentication is valid')
|
|
360
|
+
await expect(
|
|
361
|
+
CheckIsAuthenticated({ authToken: 'not-valid-token' }, {}),
|
|
362
|
+
).rejects.toThrowError(ProcedureError)
|
|
363
|
+
})
|
|
364
|
+
})
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { ProcedureError, ProcedureValidationError } from './errors.js'
|
|
2
|
+
import { computeSchema } from './schema/compute-schema.js'
|
|
3
|
+
import { Prettify, TJSONSchema, TSchemaLib } from './schema/types.js'
|
|
4
|
+
|
|
5
|
+
export type TNoContextProvided = unknown
|
|
6
|
+
|
|
7
|
+
export type TLocalContext = {
|
|
8
|
+
error: (message: string, meta?: object) => ProcedureError
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type TProcedureRegistration<TContext = unknown, TExtendedConfig = unknown> = {
|
|
12
|
+
name: string
|
|
13
|
+
config: {
|
|
14
|
+
description?: string
|
|
15
|
+
schema?: {
|
|
16
|
+
params?: TJSONSchema
|
|
17
|
+
returnType?: TJSONSchema
|
|
18
|
+
}
|
|
19
|
+
validation?: {
|
|
20
|
+
params?: (params: any) => { errors?: any[] }
|
|
21
|
+
}
|
|
22
|
+
} & TExtendedConfig
|
|
23
|
+
|
|
24
|
+
handler: (ctx: TContext, params?: any) => Promise<any>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function Procedures<TContext = TNoContextProvided, TExtendedConfig = unknown>(
|
|
28
|
+
/**
|
|
29
|
+
* Optionally provided builder to register Procedures
|
|
30
|
+
*/
|
|
31
|
+
builder?: {
|
|
32
|
+
onCreate?: (
|
|
33
|
+
procedure: Prettify<{
|
|
34
|
+
name: string
|
|
35
|
+
handler: (ctx: Prettify<TContext>, params?: any) => Promise<any>
|
|
36
|
+
config: Prettify<
|
|
37
|
+
{
|
|
38
|
+
description?: string
|
|
39
|
+
schema?: {
|
|
40
|
+
params?: TJSONSchema
|
|
41
|
+
returnType?: TJSONSchema
|
|
42
|
+
}
|
|
43
|
+
validation?: {
|
|
44
|
+
params?: (params: any) => { errors?: any[] }
|
|
45
|
+
}
|
|
46
|
+
} & TExtendedConfig
|
|
47
|
+
>
|
|
48
|
+
}>
|
|
49
|
+
) => void
|
|
50
|
+
}
|
|
51
|
+
) {
|
|
52
|
+
const procedures: Map<
|
|
53
|
+
string,
|
|
54
|
+
{
|
|
55
|
+
name: string
|
|
56
|
+
config: Prettify<
|
|
57
|
+
{
|
|
58
|
+
description?: string
|
|
59
|
+
schema?: {
|
|
60
|
+
params?: TJSONSchema
|
|
61
|
+
returnType?: TJSONSchema
|
|
62
|
+
}
|
|
63
|
+
validation?: {
|
|
64
|
+
params?: (params: any) => { errors?: any[] }
|
|
65
|
+
}
|
|
66
|
+
} & TExtendedConfig
|
|
67
|
+
>
|
|
68
|
+
handler: (ctx: Prettify<TContext>, params: any) => Promise<any>
|
|
69
|
+
}
|
|
70
|
+
> = new Map()
|
|
71
|
+
|
|
72
|
+
function Create<TName extends string, TParams, TReturnType>(
|
|
73
|
+
name: TName,
|
|
74
|
+
config: {
|
|
75
|
+
description?: string
|
|
76
|
+
schema?: {
|
|
77
|
+
params?: TParams
|
|
78
|
+
returnType?: TReturnType
|
|
79
|
+
}
|
|
80
|
+
} & TExtendedConfig,
|
|
81
|
+
handler: (
|
|
82
|
+
ctx: Prettify<TContext & TLocalContext>,
|
|
83
|
+
params: TSchemaLib<TParams>
|
|
84
|
+
) => Promise<TSchemaLib<TReturnType>>
|
|
85
|
+
) {
|
|
86
|
+
const { jsonSchema, validations } = computeSchema(name, config.schema)
|
|
87
|
+
|
|
88
|
+
const registeredProcedure = {
|
|
89
|
+
name,
|
|
90
|
+
config: {
|
|
91
|
+
// ctx: config.hook, ??? why was this here
|
|
92
|
+
...config,
|
|
93
|
+
description: config.description,
|
|
94
|
+
schema: jsonSchema,
|
|
95
|
+
validation: {
|
|
96
|
+
params: validations.params,
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
handler: async (ctx: Prettify<TContext>, params: TSchemaLib<TParams>) => {
|
|
101
|
+
try {
|
|
102
|
+
if (validations?.params) {
|
|
103
|
+
const { errors } = validations.params(params)
|
|
104
|
+
|
|
105
|
+
if (errors) {
|
|
106
|
+
throw new ProcedureValidationError(
|
|
107
|
+
name,
|
|
108
|
+
`Validation error for ${name} - ${errors.map((e) => e.message).join(', ')}`,
|
|
109
|
+
errors
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const localCtx: TLocalContext = {
|
|
115
|
+
error: (message: string, meta?: object) => {
|
|
116
|
+
return new ProcedureError(name, message, meta)
|
|
117
|
+
},
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return handler(
|
|
121
|
+
{
|
|
122
|
+
...ctx,
|
|
123
|
+
...localCtx,
|
|
124
|
+
} as Prettify<TContext & TLocalContext>,
|
|
125
|
+
params
|
|
126
|
+
)
|
|
127
|
+
} catch (error: any) {
|
|
128
|
+
if (error instanceof ProcedureError) {
|
|
129
|
+
throw error
|
|
130
|
+
} else {
|
|
131
|
+
const err = new ProcedureError(name, `Error in handler for ${name} - ${error?.message}`)
|
|
132
|
+
err.stack = error.stack
|
|
133
|
+
throw err
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
procedures.set(name, registeredProcedure)
|
|
140
|
+
|
|
141
|
+
if (builder?.onCreate) {
|
|
142
|
+
builder.onCreate(registeredProcedure)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const info = {
|
|
146
|
+
name,
|
|
147
|
+
...registeredProcedure.config,
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// return so can be called directly (ie: int/unit tests)
|
|
151
|
+
return {
|
|
152
|
+
[name]: registeredProcedure.handler,
|
|
153
|
+
procedure: registeredProcedure.handler,
|
|
154
|
+
info,
|
|
155
|
+
} as {
|
|
156
|
+
[K in TName]: (ctx: Prettify<TContext>, params: TSchemaLib<TParams>) => Promise<TSchemaLib<TReturnType>>
|
|
157
|
+
} & {
|
|
158
|
+
procedure: (ctx: Prettify<TContext>, params: TSchemaLib<TParams>) => Promise<TSchemaLib<TReturnType>>
|
|
159
|
+
info: {
|
|
160
|
+
name: TName
|
|
161
|
+
description?: string
|
|
162
|
+
schema: {
|
|
163
|
+
params?: TParams
|
|
164
|
+
returnType?: TReturnType
|
|
165
|
+
}
|
|
166
|
+
validation?: {
|
|
167
|
+
params?: (params: any) => { errors?: any[] }
|
|
168
|
+
}
|
|
169
|
+
} & TExtendedConfig
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
getProcedures: () => {
|
|
175
|
+
return procedures
|
|
176
|
+
},
|
|
177
|
+
|
|
178
|
+
Create,
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { Type } from 'typebox'
|
|
3
|
+
import { v } from 'suretype'
|
|
4
|
+
import { computeSchema } from './compute-schema.js'
|
|
5
|
+
import { ProcedureRegistrationError } from '../errors.js'
|
|
6
|
+
|
|
7
|
+
describe('computeSchema', () => {
|
|
8
|
+
it('should return empty schema and validations when no schema provided', () => {
|
|
9
|
+
const result = computeSchema('test-procedure')
|
|
10
|
+
|
|
11
|
+
expect(result).toEqual({
|
|
12
|
+
jsonSchema: { params: undefined, returnType: undefined },
|
|
13
|
+
validations: {}
|
|
14
|
+
})
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
describe('with Typebox schema', () => {
|
|
18
|
+
it('should correctly process params schema', () => {
|
|
19
|
+
const schema = {
|
|
20
|
+
params: Type.Object({
|
|
21
|
+
name: Type.String(),
|
|
22
|
+
age: Type.Number()
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const result = computeSchema('test-procedure', schema)
|
|
27
|
+
|
|
28
|
+
expect(result.jsonSchema.params).toBeDefined()
|
|
29
|
+
expect(result.validations.params).toBeDefined()
|
|
30
|
+
|
|
31
|
+
// Test validation function
|
|
32
|
+
const validInput = { name: 'John', age: 30 }
|
|
33
|
+
expect(result.validations.params?.(validInput).errors).toBeUndefined()
|
|
34
|
+
|
|
35
|
+
// Test invalid input
|
|
36
|
+
const invalidInput = { name: 123, age: 'invalid' }
|
|
37
|
+
expect(result.validations.params?.(invalidInput).errors).toBeDefined()
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('should correctly process returnType schema', () => {
|
|
41
|
+
const schema = {
|
|
42
|
+
returnType: Type.Object({
|
|
43
|
+
result: Type.Boolean()
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const result = computeSchema('test-procedure', schema)
|
|
48
|
+
|
|
49
|
+
expect(result.jsonSchema.returnType).toBeDefined()
|
|
50
|
+
expect(result.validations.params).toBeUndefined()
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
describe('with Suretype schema', () => {
|
|
55
|
+
it('should correctly process params schema', () => {
|
|
56
|
+
const schema = {
|
|
57
|
+
params: v.object({
|
|
58
|
+
name: v.string(),
|
|
59
|
+
age: v.number()
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const result = computeSchema('test-procedure', schema)
|
|
64
|
+
|
|
65
|
+
expect(result.jsonSchema.params).toBeDefined()
|
|
66
|
+
expect(result.validations.params).toBeDefined()
|
|
67
|
+
|
|
68
|
+
// Test validation function
|
|
69
|
+
const validInput = { name: 'John', age: 30 }
|
|
70
|
+
expect(result.validations.params?.(validInput).errors).toBeUndefined()
|
|
71
|
+
|
|
72
|
+
// Test invalid input
|
|
73
|
+
const invalidInput = { name: 123, age: 'invalid' }
|
|
74
|
+
expect(result.validations.params?.(invalidInput).errors).toBeDefined()
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
describe('error handling', () => {
|
|
79
|
+
it('should throw ProcedureRegistrationError for invalid schema', () => {
|
|
80
|
+
const invalidSchema = {
|
|
81
|
+
params: {
|
|
82
|
+
type: 'invalid-schema-type'
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
expect(() => computeSchema('test-procedure', invalidSchema))
|
|
87
|
+
.toThrow(ProcedureRegistrationError)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('should include procedure name in error message', () => {
|
|
91
|
+
const invalidSchema = {
|
|
92
|
+
params: {
|
|
93
|
+
type: 'invalid-schema-type'
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
computeSchema('test-procedure', invalidSchema)
|
|
99
|
+
} catch (error: any) {
|
|
100
|
+
expect(error instanceof ProcedureRegistrationError).toBe(true)
|
|
101
|
+
expect(error.message).toContain('test-procedure')
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
describe('combined schemas', () => {
|
|
107
|
+
it('should handle both params and returnType schemas', () => {
|
|
108
|
+
const schema = {
|
|
109
|
+
params: Type.Object({
|
|
110
|
+
input: Type.String()
|
|
111
|
+
}),
|
|
112
|
+
returnType: Type.Object({
|
|
113
|
+
output: Type.Boolean()
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const result = computeSchema('test-procedure', schema)
|
|
118
|
+
|
|
119
|
+
expect(result.jsonSchema.params).toBeDefined()
|
|
120
|
+
expect(result.jsonSchema.returnType).toBeDefined()
|
|
121
|
+
expect(result.validations.params).toBeDefined()
|
|
122
|
+
|
|
123
|
+
// Test params validation
|
|
124
|
+
const validInput = { input: 'test' }
|
|
125
|
+
expect(result.validations.params?.(validInput).errors).toBeUndefined()
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
})
|