ts-procedures 2.1.1 → 3.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/build/errors.d.ts +2 -1
- package/build/errors.js +3 -2
- package/build/errors.js.map +1 -1
- package/build/errors.test.d.ts +1 -0
- package/build/errors.test.js +40 -0
- package/build/errors.test.js.map +1 -0
- package/build/implementations/http/express-rpc/index.d.ts +3 -2
- package/build/implementations/http/express-rpc/index.js +6 -6
- package/build/implementations/http/express-rpc/index.js.map +1 -1
- package/build/implementations/http/express-rpc/index.test.js +93 -93
- package/build/implementations/http/express-rpc/index.test.js.map +1 -1
- package/build/implementations/http/hono-rpc/index.d.ts +83 -0
- package/build/implementations/http/hono-rpc/index.js +148 -0
- package/build/implementations/http/hono-rpc/index.js.map +1 -0
- package/build/implementations/http/hono-rpc/index.test.d.ts +1 -0
- package/build/implementations/http/hono-rpc/index.test.js +647 -0
- package/build/implementations/http/hono-rpc/index.test.js.map +1 -0
- package/build/implementations/http/hono-rpc/types.d.ts +28 -0
- package/build/implementations/http/hono-rpc/types.js +2 -0
- package/build/implementations/http/hono-rpc/types.js.map +1 -0
- package/build/implementations/types.d.ts +1 -1
- package/build/index.d.ts +12 -0
- package/build/index.js +29 -7
- package/build/index.js.map +1 -1
- package/build/index.test.js +65 -0
- package/build/index.test.js.map +1 -1
- package/build/schema/parser.js +3 -0
- package/build/schema/parser.js.map +1 -1
- package/build/schema/parser.test.js +18 -0
- package/build/schema/parser.test.js.map +1 -1
- package/package.json +10 -5
- package/src/errors.test.ts +53 -0
- package/src/errors.ts +4 -2
- package/src/implementations/http/README.md +172 -0
- package/src/implementations/http/express-rpc/README.md +151 -242
- package/src/implementations/http/express-rpc/index.test.ts +93 -93
- package/src/implementations/http/express-rpc/index.ts +15 -7
- package/src/implementations/http/hono-rpc/README.md +293 -0
- package/src/implementations/http/hono-rpc/index.test.ts +847 -0
- package/src/implementations/http/hono-rpc/index.ts +202 -0
- package/src/implementations/http/hono-rpc/types.ts +33 -0
- package/src/implementations/types.ts +2 -1
- package/src/index.test.ts +83 -0
- package/src/index.ts +34 -8
- package/src/schema/parser.test.ts +26 -0
- package/src/schema/parser.ts +5 -1
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { Hono, Context } from 'hono'
|
|
2
|
+
import { kebabCase } from 'es-toolkit/string'
|
|
3
|
+
import { Procedures, TProcedureRegistration } from '../../../index.js'
|
|
4
|
+
import { RPCConfig, RPCHttpRouteDoc } from '../../types.js'
|
|
5
|
+
import { castArray } from 'es-toolkit/compat'
|
|
6
|
+
import { HonoFactoryItem, ExtractContext, ProceduresFactory } from './types.js'
|
|
7
|
+
|
|
8
|
+
export type { RPCConfig, RPCHttpRouteDoc }
|
|
9
|
+
|
|
10
|
+
export type HonoRPCAppBuilderConfig = {
|
|
11
|
+
/**
|
|
12
|
+
* An existing Hono application instance to use.
|
|
13
|
+
* If not provided, a new instance will be created.
|
|
14
|
+
*/
|
|
15
|
+
app?: Hono
|
|
16
|
+
/** Optional path prefix for all RPC routes. */
|
|
17
|
+
pathPrefix?: string
|
|
18
|
+
onRequestStart?: (c: Context) => void
|
|
19
|
+
onRequestEnd?: (c: Context) => void
|
|
20
|
+
onSuccess?: (procedure: TProcedureRegistration, c: Context) => void
|
|
21
|
+
error?: (
|
|
22
|
+
procedure: TProcedureRegistration,
|
|
23
|
+
c: Context,
|
|
24
|
+
error: Error
|
|
25
|
+
) => Response | Promise<Response>
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Builder class for creating a Hono application with RPC routes.
|
|
30
|
+
*
|
|
31
|
+
* Usage:
|
|
32
|
+
* const PublicRPC = Procedures<PublicRPCContext, RPCConfig>()
|
|
33
|
+
* const ProtectedRPC = Procedures<ProtectedRPCContext, RPCConfig>()
|
|
34
|
+
*
|
|
35
|
+
* const rpcApp = new HonoRPCAppBuilder()
|
|
36
|
+
* .register(PublicRPC, (c): Promise<PublicRPCContext> => { /* context resolution logic * / })
|
|
37
|
+
* .register(ProtectedRPC, (c): Promise<ProtectedRPCContext> => { /* context resolution logic * / })
|
|
38
|
+
* .build();
|
|
39
|
+
*
|
|
40
|
+
* const app = rpcApp.app; // Hono application
|
|
41
|
+
* const docs = rpcApp.docs; // RPC route documentation
|
|
42
|
+
*/
|
|
43
|
+
export class HonoRPCAppBuilder {
|
|
44
|
+
/**
|
|
45
|
+
* Constructor for HonoRPCAppBuilder.
|
|
46
|
+
*
|
|
47
|
+
* @param config
|
|
48
|
+
*/
|
|
49
|
+
constructor(readonly config?: HonoRPCAppBuilderConfig) {
|
|
50
|
+
if (config?.app) {
|
|
51
|
+
this._app = config.app
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (config?.onRequestStart) {
|
|
55
|
+
this._app.use('*', async (c, next) => {
|
|
56
|
+
config.onRequestStart!(c)
|
|
57
|
+
await next()
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (config?.onRequestEnd) {
|
|
62
|
+
this._app.use('*', async (c, next) => {
|
|
63
|
+
await next()
|
|
64
|
+
config.onRequestEnd!(c)
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Generates the RPC route path based on the RPC configuration.
|
|
71
|
+
* The RPCConfig name can be a string or an array of strings to form nested paths.
|
|
72
|
+
*
|
|
73
|
+
* Example
|
|
74
|
+
* name: ['string', 'string-string', 'string']
|
|
75
|
+
* path: /string/string-string/string/version
|
|
76
|
+
* @param config
|
|
77
|
+
*/
|
|
78
|
+
static makeRPCHttpRoutePath({
|
|
79
|
+
name,
|
|
80
|
+
config,
|
|
81
|
+
prefix,
|
|
82
|
+
}: {
|
|
83
|
+
name: string
|
|
84
|
+
prefix?: string
|
|
85
|
+
config: RPCConfig
|
|
86
|
+
}) {
|
|
87
|
+
const normalizedPrefix = prefix ? (prefix.startsWith('/') ? prefix : `/${prefix}`) : ''
|
|
88
|
+
|
|
89
|
+
return `${normalizedPrefix}/${castArray(config.scope).map(kebabCase).join('/')}/${kebabCase(name)}/${String(config.version).trim()}`
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Instance method wrapper for makeRPCHttpRoutePath that uses the builder's pathPrefix.
|
|
94
|
+
* @param config - The RPC configuration
|
|
95
|
+
*/
|
|
96
|
+
makeRPCHttpRoutePath(name: string, config: RPCConfig): string {
|
|
97
|
+
return HonoRPCAppBuilder.makeRPCHttpRoutePath({
|
|
98
|
+
name,
|
|
99
|
+
config,
|
|
100
|
+
prefix: this.config?.pathPrefix,
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private factories: HonoFactoryItem<any>[] = []
|
|
105
|
+
|
|
106
|
+
private _app: Hono = new Hono()
|
|
107
|
+
private _docs: RPCHttpRouteDoc[] = []
|
|
108
|
+
|
|
109
|
+
get app(): Hono {
|
|
110
|
+
return this._app
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
get docs(): RPCHttpRouteDoc[] {
|
|
114
|
+
return this._docs
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Registers a procedure factory with its context.
|
|
119
|
+
* @param factory - The procedure factory created by Procedures<Context, RPCConfig>()
|
|
120
|
+
* @param factoryContext - The context for procedure handlers. Can be a direct value,
|
|
121
|
+
* a sync function (c) => Context, or an async function (c) => Promise<Context>
|
|
122
|
+
*/
|
|
123
|
+
register<TFactory extends ProceduresFactory>(
|
|
124
|
+
factory: TFactory,
|
|
125
|
+
factoryContext:
|
|
126
|
+
| ExtractContext<TFactory>
|
|
127
|
+
| ((c: Context) => ExtractContext<TFactory> | Promise<ExtractContext<TFactory>>)
|
|
128
|
+
): this {
|
|
129
|
+
this.factories.push({ factory, factoryContext } as HonoFactoryItem<any>)
|
|
130
|
+
return this
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Builds and returns the Hono application with registered RPC routes.
|
|
135
|
+
* @return Hono
|
|
136
|
+
*/
|
|
137
|
+
build(): Hono {
|
|
138
|
+
this.factories.forEach(({ factory, factoryContext }) => {
|
|
139
|
+
factory.getProcedures().map((procedure: TProcedureRegistration<any, RPCConfig>) => {
|
|
140
|
+
const route = this.buildRpcHttpRouteDoc(procedure)
|
|
141
|
+
|
|
142
|
+
this._docs.push(route)
|
|
143
|
+
|
|
144
|
+
this._app.post(route.path, async (c) => {
|
|
145
|
+
try {
|
|
146
|
+
const context =
|
|
147
|
+
typeof factoryContext === 'function'
|
|
148
|
+
? await factoryContext(c)
|
|
149
|
+
: (factoryContext as ExtractContext<typeof factory>)
|
|
150
|
+
|
|
151
|
+
// Hono uses c.req.json() for body parsing
|
|
152
|
+
const body = await c.req.json().catch(() => ({}))
|
|
153
|
+
const result = await procedure.handler(context, body)
|
|
154
|
+
|
|
155
|
+
if (this.config?.onSuccess) {
|
|
156
|
+
this.config.onSuccess(procedure, c)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Hono returns Response objects via c.json()
|
|
160
|
+
return c.json(result)
|
|
161
|
+
} catch (error) {
|
|
162
|
+
if (this.config?.error) {
|
|
163
|
+
return this.config.error(procedure, c, error as Error)
|
|
164
|
+
}
|
|
165
|
+
// Default error handling
|
|
166
|
+
return c.json({ error: (error as Error).message }, 500)
|
|
167
|
+
}
|
|
168
|
+
})
|
|
169
|
+
})
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
return this._app
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Generates the RPC HTTP route for the given procedure.
|
|
177
|
+
* @param procedure
|
|
178
|
+
*/
|
|
179
|
+
private buildRpcHttpRouteDoc(procedure: TProcedureRegistration<any, RPCConfig>): RPCHttpRouteDoc {
|
|
180
|
+
const { config } = procedure
|
|
181
|
+
const path = HonoRPCAppBuilder.makeRPCHttpRoutePath({
|
|
182
|
+
name: procedure.name,
|
|
183
|
+
config,
|
|
184
|
+
prefix: this.config?.pathPrefix,
|
|
185
|
+
})
|
|
186
|
+
const method = 'post' // RPCs use POST method
|
|
187
|
+
const jsonSchema: { body?: object; response?: object } = {}
|
|
188
|
+
|
|
189
|
+
if (config.schema?.params) {
|
|
190
|
+
jsonSchema.body = config.schema.params
|
|
191
|
+
}
|
|
192
|
+
if (config.schema?.returnType) {
|
|
193
|
+
jsonSchema.response = config.schema.returnType
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
path,
|
|
198
|
+
method,
|
|
199
|
+
jsonSchema,
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { RPCConfig } from '../../types.js'
|
|
2
|
+
import { Procedures } from '../../../index.js'
|
|
3
|
+
import { Context } from 'hono'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Extracts the TContext type from a Procedures factory return type.
|
|
7
|
+
* Uses the first parameter of the handler function to infer the context type.
|
|
8
|
+
*/
|
|
9
|
+
export type ExtractContext<TFactory> = TFactory extends {
|
|
10
|
+
getProcedures: () => Array<{ handler: (ctx: infer TContext, ...args: any[]) => any }>
|
|
11
|
+
}
|
|
12
|
+
? TContext
|
|
13
|
+
: never
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Minimal structural type for a Procedures factory.
|
|
17
|
+
* Uses explicit `any` types to avoid variance issues with generic constraints.
|
|
18
|
+
*/
|
|
19
|
+
export type ProceduresFactory = {
|
|
20
|
+
getProcedures: () => Array<{
|
|
21
|
+
name: string
|
|
22
|
+
config: any
|
|
23
|
+
handler: (ctx: any, params?: any) => Promise<any>
|
|
24
|
+
}>
|
|
25
|
+
Create: (...args: any[]) => any
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type HonoFactoryItem<TFactory = ReturnType<typeof Procedures<any, RPCConfig>>> = {
|
|
29
|
+
factory: TFactory
|
|
30
|
+
factoryContext:
|
|
31
|
+
| ExtractContext<TFactory>
|
|
32
|
+
| ((c: Context) => ExtractContext<TFactory> | Promise<ExtractContext<TFactory>>)
|
|
33
|
+
}
|
package/src/index.test.ts
CHANGED
|
@@ -363,4 +363,87 @@ describe('Procedures', () => {
|
|
|
363
363
|
CheckIsAuthenticated({ authToken: 'not-valid-token' }, {}),
|
|
364
364
|
).rejects.toThrowError(ProcedureError)
|
|
365
365
|
})
|
|
366
|
+
|
|
367
|
+
test('Procedures - duplicate registration throws before schema computation', () => {
|
|
368
|
+
const { Create } = Procedures()
|
|
369
|
+
|
|
370
|
+
Create('DuplicateTest', {}, async () => 'first')
|
|
371
|
+
|
|
372
|
+
expect(() => {
|
|
373
|
+
Create('DuplicateTest', {}, async () => 'second')
|
|
374
|
+
}).toThrow('Procedure with name DuplicateTest is already registered')
|
|
375
|
+
})
|
|
376
|
+
|
|
377
|
+
test('Procedures - wrapped errors preserve cause', async () => {
|
|
378
|
+
const { Create } = Procedures()
|
|
379
|
+
const originalError = new Error('Database connection failed')
|
|
380
|
+
;(originalError as any).code = 'ECONNREFUSED'
|
|
381
|
+
|
|
382
|
+
const { TestCause } = Create('TestCause', {}, async () => {
|
|
383
|
+
throw originalError
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
try {
|
|
387
|
+
await TestCause({}, {})
|
|
388
|
+
} catch (e: any) {
|
|
389
|
+
expect(e).toBeInstanceOf(ProcedureError)
|
|
390
|
+
expect(e.cause).toBe(originalError)
|
|
391
|
+
expect(e.cause.code).toBe('ECONNREFUSED')
|
|
392
|
+
}
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
test('Procedures - getProcedure returns specific procedure', () => {
|
|
396
|
+
const { Create, getProcedure } = Procedures()
|
|
397
|
+
|
|
398
|
+
Create('FindMe', {}, async () => 'found')
|
|
399
|
+
|
|
400
|
+
const proc = getProcedure('FindMe')
|
|
401
|
+
expect(proc).toBeDefined()
|
|
402
|
+
expect(proc?.name).toBe('FindMe')
|
|
403
|
+
|
|
404
|
+
expect(getProcedure('NotFound')).toBeUndefined()
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
test('Procedures - removeProcedure allows re-registration', () => {
|
|
408
|
+
const { Create, removeProcedure, getProcedure } = Procedures()
|
|
409
|
+
|
|
410
|
+
Create('Removable', {}, async () => 'v1')
|
|
411
|
+
expect(getProcedure('Removable')).toBeDefined()
|
|
412
|
+
|
|
413
|
+
const removed = removeProcedure('Removable')
|
|
414
|
+
expect(removed).toBe(true)
|
|
415
|
+
expect(getProcedure('Removable')).toBeUndefined()
|
|
416
|
+
|
|
417
|
+
// Can now re-register
|
|
418
|
+
Create('Removable', {}, async () => 'v2')
|
|
419
|
+
expect(getProcedure('Removable')).toBeDefined()
|
|
420
|
+
})
|
|
421
|
+
|
|
422
|
+
test('Procedures - clear removes all procedures', () => {
|
|
423
|
+
const { Create, getProcedures, clear } = Procedures()
|
|
424
|
+
|
|
425
|
+
Create('One', {}, async () => '1')
|
|
426
|
+
Create('Two', {}, async () => '2')
|
|
427
|
+
expect(getProcedures().length).toBe(2)
|
|
428
|
+
|
|
429
|
+
clear()
|
|
430
|
+
expect(getProcedures().length).toBe(0)
|
|
431
|
+
})
|
|
432
|
+
|
|
433
|
+
test('Procedures - ctx.error still works after optimization', async () => {
|
|
434
|
+
const { Create } = Procedures()
|
|
435
|
+
|
|
436
|
+
const { ErrorTest } = Create('ErrorTest', {}, async (ctx) => {
|
|
437
|
+
throw ctx.error('Custom error message', { code: 'ERR_001' })
|
|
438
|
+
})
|
|
439
|
+
|
|
440
|
+
try {
|
|
441
|
+
await ErrorTest({}, {})
|
|
442
|
+
} catch (e: any) {
|
|
443
|
+
expect(e).toBeInstanceOf(ProcedureError)
|
|
444
|
+
expect(e.message).toBe('Custom error message')
|
|
445
|
+
expect(e.procedureName).toBe('ErrorTest')
|
|
446
|
+
expect(e.meta).toEqual({ code: 'ERR_001' })
|
|
447
|
+
}
|
|
448
|
+
})
|
|
366
449
|
})
|
package/src/index.ts
CHANGED
|
@@ -68,8 +68,18 @@ export function Procedures<TContext = TNoContextProvided, TExtendedConfig = unkn
|
|
|
68
68
|
params: TSchemaLib<TParams>
|
|
69
69
|
) => Promise<TSchemaLib<TReturnType>>
|
|
70
70
|
) {
|
|
71
|
+
// BEFORE computeSchema - fail fast on duplicate
|
|
72
|
+
if (procedures.has(name)) {
|
|
73
|
+
throw new Error(`Procedure with name ${name} is already registered`)
|
|
74
|
+
}
|
|
75
|
+
|
|
71
76
|
const { jsonSchema, validations } = computeSchema(name, config.schema)
|
|
72
77
|
|
|
78
|
+
// Create error factory once at registration time (outside handler)
|
|
79
|
+
const errorFactory = (message: string, meta?: object) => {
|
|
80
|
+
return new ProcedureError(name, message, meta)
|
|
81
|
+
}
|
|
82
|
+
|
|
73
83
|
const registeredProcedure = {
|
|
74
84
|
name,
|
|
75
85
|
config: {
|
|
@@ -97,12 +107,10 @@ export function Procedures<TContext = TNoContextProvided, TExtendedConfig = unkn
|
|
|
97
107
|
}
|
|
98
108
|
|
|
99
109
|
const localCtx: TLocalContext = {
|
|
100
|
-
error:
|
|
101
|
-
return new ProcedureError(name, message, meta)
|
|
102
|
-
},
|
|
110
|
+
error: errorFactory, // Reuse pre-created factory
|
|
103
111
|
}
|
|
104
112
|
|
|
105
|
-
return handler(
|
|
113
|
+
return await handler(
|
|
106
114
|
{
|
|
107
115
|
...ctx,
|
|
108
116
|
...localCtx,
|
|
@@ -114,6 +122,7 @@ export function Procedures<TContext = TNoContextProvided, TExtendedConfig = unkn
|
|
|
114
122
|
throw error
|
|
115
123
|
} else {
|
|
116
124
|
const err = new ProcedureError(name, `Error in handler for ${name} - ${error?.message}`)
|
|
125
|
+
err.cause = error // Preserve original error
|
|
117
126
|
err.stack = error.stack
|
|
118
127
|
throw err
|
|
119
128
|
}
|
|
@@ -121,10 +130,6 @@ export function Procedures<TContext = TNoContextProvided, TExtendedConfig = unkn
|
|
|
121
130
|
},
|
|
122
131
|
}
|
|
123
132
|
|
|
124
|
-
if (procedures.has(name)) {
|
|
125
|
-
throw new Error(`Procedure with name ${name} is already registered`)
|
|
126
|
-
}
|
|
127
|
-
|
|
128
133
|
procedures.set(name, registeredProcedure)
|
|
129
134
|
|
|
130
135
|
if (builder?.onCreate) {
|
|
@@ -167,6 +172,27 @@ export function Procedures<TContext = TNoContextProvided, TExtendedConfig = unkn
|
|
|
167
172
|
return Array.from(procedures.values())
|
|
168
173
|
},
|
|
169
174
|
|
|
175
|
+
/**
|
|
176
|
+
* Get a specific procedure by name
|
|
177
|
+
*/
|
|
178
|
+
getProcedure: (name: string) => {
|
|
179
|
+
return procedures.get(name)
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Remove a procedure by name
|
|
184
|
+
*/
|
|
185
|
+
removeProcedure: (name: string) => {
|
|
186
|
+
return procedures.delete(name)
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Clear all registered procedures
|
|
191
|
+
*/
|
|
192
|
+
clear: () => {
|
|
193
|
+
procedures.clear()
|
|
194
|
+
},
|
|
195
|
+
|
|
170
196
|
Create,
|
|
171
197
|
}
|
|
172
198
|
}
|
|
@@ -153,4 +153,30 @@ describe('schemaParser', () => {
|
|
|
153
153
|
).toMatchObject({})
|
|
154
154
|
expect(schema.validation.params?.({}).errors?.[0]?.message).toMatch(/must have required property 'a'/)
|
|
155
155
|
})
|
|
156
|
+
|
|
157
|
+
test('validation returns error if validator fails to initialize', async () => {
|
|
158
|
+
// Create a schema that will pass extraction but creates a validation function
|
|
159
|
+
// that handles the uninitialized case gracefully
|
|
160
|
+
let parseErrorCalled = false
|
|
161
|
+
|
|
162
|
+
const result = schemaParser(
|
|
163
|
+
{
|
|
164
|
+
// Using a valid schema to get through extraction, but we'll test the guard
|
|
165
|
+
params: Type.Object({ name: Type.String() }),
|
|
166
|
+
},
|
|
167
|
+
() => {
|
|
168
|
+
parseErrorCalled = true
|
|
169
|
+
},
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
// The validator should be initialized for valid schemas
|
|
173
|
+
expect(result.validation.params).toBeDefined()
|
|
174
|
+
|
|
175
|
+
// Test that the validation function works correctly
|
|
176
|
+
const validResult = result.validation.params?.({ name: 'test' })
|
|
177
|
+
expect(validResult?.errors).toBeUndefined()
|
|
178
|
+
|
|
179
|
+
const invalidResult = result.validation.params?.({})
|
|
180
|
+
expect(invalidResult?.errors).toBeDefined()
|
|
181
|
+
})
|
|
156
182
|
})
|
package/src/schema/parser.ts
CHANGED
|
@@ -45,7 +45,7 @@ export function schemaParser(
|
|
|
45
45
|
params: `Error extracting json schema schema.params - schema.params might be empty or it is not a valid suretype or typebox type`,
|
|
46
46
|
})
|
|
47
47
|
} else {
|
|
48
|
-
let paramsValidator: AJV.ValidateFunction
|
|
48
|
+
let paramsValidator: AJV.ValidateFunction | undefined
|
|
49
49
|
|
|
50
50
|
try {
|
|
51
51
|
paramsValidator = ajv.compile(jsonSchema.params as TJSONSchema)
|
|
@@ -56,6 +56,10 @@ export function schemaParser(
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
validation.params = (params: any) => {
|
|
59
|
+
if (!paramsValidator) {
|
|
60
|
+
return { errors: [{ message: 'Validator not initialized', keyword: 'internal' } as TSchemaValidationError] }
|
|
61
|
+
}
|
|
62
|
+
|
|
59
63
|
const valid = paramsValidator(params)
|
|
60
64
|
|
|
61
65
|
if (!valid) {
|