ts-procedures 5.9.1 → 5.10.2
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 +1 -1
- package/agent_config/bin/postinstall.mjs +3 -3
- package/agent_config/bin/setup.mjs +22 -11
- package/agent_config/claude-code/agents/ts-procedures-architect.md +46 -101
- package/agent_config/claude-code/skills/{guide → ts-procedures}/SKILL.md +50 -35
- package/agent_config/claude-code/skills/{guide → ts-procedures}/anti-patterns.md +6 -5
- package/agent_config/claude-code/skills/{guide → ts-procedures}/api-reference.md +60 -49
- package/agent_config/claude-code/skills/ts-procedures-review/SKILL.md +48 -0
- package/agent_config/claude-code/skills/{scaffold → ts-procedures-scaffold}/SKILL.md +19 -24
- package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/client.md +115 -0
- package/agent_config/lib/install-claude.mjs +35 -87
- package/build/src/client/call.d.ts +14 -0
- package/build/src/client/call.js +47 -0
- package/build/src/client/call.js.map +1 -0
- package/build/src/client/call.test.d.ts +1 -0
- package/build/src/client/call.test.js +124 -0
- package/build/src/client/call.test.js.map +1 -0
- package/build/src/client/errors.d.ts +25 -0
- package/build/src/client/errors.js +33 -0
- package/build/src/client/errors.js.map +1 -0
- package/build/src/client/errors.test.d.ts +1 -0
- package/build/src/client/errors.test.js +41 -0
- package/build/src/client/errors.test.js.map +1 -0
- package/build/src/client/fetch-adapter.d.ts +12 -0
- package/build/src/client/fetch-adapter.js +156 -0
- package/build/src/client/fetch-adapter.js.map +1 -0
- package/build/src/client/fetch-adapter.test.d.ts +1 -0
- package/build/src/client/fetch-adapter.test.js +271 -0
- package/build/src/client/fetch-adapter.test.js.map +1 -0
- package/build/src/client/hooks.d.ts +17 -0
- package/build/src/client/hooks.js +40 -0
- package/build/src/client/hooks.js.map +1 -0
- package/build/src/client/hooks.test.d.ts +1 -0
- package/build/src/client/hooks.test.js +163 -0
- package/build/src/client/hooks.test.js.map +1 -0
- package/build/src/client/index.d.ts +22 -0
- package/build/src/client/index.js +67 -0
- package/build/src/client/index.js.map +1 -0
- package/build/src/client/index.test.d.ts +1 -0
- package/build/src/client/index.test.js +231 -0
- package/build/src/client/index.test.js.map +1 -0
- package/build/src/client/request-builder.d.ts +13 -0
- package/build/src/client/request-builder.js +53 -0
- package/build/src/client/request-builder.js.map +1 -0
- package/build/src/client/request-builder.test.d.ts +1 -0
- package/build/src/client/request-builder.test.js +160 -0
- package/build/src/client/request-builder.test.js.map +1 -0
- package/build/src/client/stream.d.ts +27 -0
- package/build/src/client/stream.js +118 -0
- package/build/src/client/stream.js.map +1 -0
- package/build/src/client/stream.test.d.ts +1 -0
- package/build/src/client/stream.test.js +228 -0
- package/build/src/client/stream.test.js.map +1 -0
- package/build/src/client/types.d.ts +78 -0
- package/build/src/client/types.js +3 -0
- package/build/src/client/types.js.map +1 -0
- package/build/src/codegen/bin/cli.d.ts +45 -0
- package/build/src/codegen/bin/cli.js +246 -0
- package/build/src/codegen/bin/cli.js.map +1 -0
- package/build/src/codegen/bin/cli.test.d.ts +1 -0
- package/build/src/codegen/bin/cli.test.js +220 -0
- package/build/src/codegen/bin/cli.test.js.map +1 -0
- package/build/src/codegen/constants.d.ts +1 -0
- package/build/src/codegen/constants.js +2 -0
- package/build/src/codegen/constants.js.map +1 -0
- package/build/src/codegen/e2e.test.d.ts +1 -0
- package/build/src/codegen/e2e.test.js +464 -0
- package/build/src/codegen/e2e.test.js.map +1 -0
- package/build/src/codegen/emit-client-runtime.d.ts +9 -0
- package/build/src/codegen/emit-client-runtime.js +99 -0
- package/build/src/codegen/emit-client-runtime.js.map +1 -0
- package/build/src/codegen/emit-client-runtime.test.d.ts +1 -0
- package/build/src/codegen/emit-client-runtime.test.js +78 -0
- package/build/src/codegen/emit-client-runtime.test.js.map +1 -0
- package/build/src/codegen/emit-client-types.d.ts +8 -0
- package/build/src/codegen/emit-client-types.js +25 -0
- package/build/src/codegen/emit-client-types.js.map +1 -0
- package/build/src/codegen/emit-client-types.test.d.ts +1 -0
- package/build/src/codegen/emit-client-types.test.js +33 -0
- package/build/src/codegen/emit-client-types.test.js.map +1 -0
- package/build/src/codegen/emit-errors.d.ts +19 -0
- package/build/src/codegen/emit-errors.js +59 -0
- package/build/src/codegen/emit-errors.js.map +1 -0
- package/build/src/codegen/emit-errors.test.d.ts +1 -0
- package/build/src/codegen/emit-errors.test.js +175 -0
- package/build/src/codegen/emit-errors.test.js.map +1 -0
- package/build/src/codegen/emit-index.d.ts +12 -0
- package/build/src/codegen/emit-index.js +41 -0
- package/build/src/codegen/emit-index.js.map +1 -0
- package/build/src/codegen/emit-index.test.d.ts +1 -0
- package/build/src/codegen/emit-index.test.js +106 -0
- package/build/src/codegen/emit-index.test.js.map +1 -0
- package/build/src/codegen/emit-scope.d.ts +15 -0
- package/build/src/codegen/emit-scope.js +299 -0
- package/build/src/codegen/emit-scope.js.map +1 -0
- package/build/src/codegen/emit-scope.test.d.ts +1 -0
- package/build/src/codegen/emit-scope.test.js +559 -0
- package/build/src/codegen/emit-scope.test.js.map +1 -0
- package/build/src/codegen/emit-types.d.ts +43 -0
- package/build/src/codegen/emit-types.js +111 -0
- package/build/src/codegen/emit-types.js.map +1 -0
- package/build/src/codegen/emit-types.test.d.ts +1 -0
- package/build/src/codegen/emit-types.test.js +184 -0
- package/build/src/codegen/emit-types.test.js.map +1 -0
- package/build/src/codegen/group-routes.d.ts +23 -0
- package/build/src/codegen/group-routes.js +46 -0
- package/build/src/codegen/group-routes.js.map +1 -0
- package/build/src/codegen/group-routes.test.d.ts +1 -0
- package/build/src/codegen/group-routes.test.js +131 -0
- package/build/src/codegen/group-routes.test.js.map +1 -0
- package/build/src/codegen/index.d.ts +15 -0
- package/build/src/codegen/index.js +16 -0
- package/build/src/codegen/index.js.map +1 -0
- package/build/src/codegen/naming.d.ts +7 -0
- package/build/src/codegen/naming.js +21 -0
- package/build/src/codegen/naming.js.map +1 -0
- package/build/src/codegen/naming.test.d.ts +1 -0
- package/build/src/codegen/naming.test.js +40 -0
- package/build/src/codegen/naming.test.js.map +1 -0
- package/build/src/codegen/pipeline.d.ts +17 -0
- package/build/src/codegen/pipeline.js +78 -0
- package/build/src/codegen/pipeline.js.map +1 -0
- package/build/src/codegen/pipeline.test.d.ts +1 -0
- package/build/src/codegen/pipeline.test.js +269 -0
- package/build/src/codegen/pipeline.test.js.map +1 -0
- package/build/src/codegen/resolve-envelope.d.ts +7 -0
- package/build/src/codegen/resolve-envelope.js +46 -0
- package/build/src/codegen/resolve-envelope.js.map +1 -0
- package/build/src/codegen/resolve-envelope.test.d.ts +1 -0
- package/build/src/codegen/resolve-envelope.test.js +69 -0
- package/build/src/codegen/resolve-envelope.test.js.map +1 -0
- package/build/src/errors.d.ts +33 -0
- package/build/src/errors.js +91 -0
- package/build/src/errors.js.map +1 -0
- package/build/src/errors.test.d.ts +1 -0
- package/build/src/errors.test.js +122 -0
- package/build/src/errors.test.js.map +1 -0
- package/build/src/exports.d.ts +7 -0
- package/build/src/exports.js +8 -0
- package/build/src/exports.js.map +1 -0
- package/build/src/implementations/http/doc-registry.d.ts +12 -0
- package/build/src/implementations/http/doc-registry.js +114 -0
- package/build/src/implementations/http/doc-registry.js.map +1 -0
- package/build/src/implementations/http/doc-registry.test.d.ts +1 -0
- package/build/src/implementations/http/doc-registry.test.js +347 -0
- package/build/src/implementations/http/doc-registry.test.js.map +1 -0
- package/build/src/implementations/http/express-rpc/index.d.ts +94 -0
- package/build/src/implementations/http/express-rpc/index.js +185 -0
- package/build/src/implementations/http/express-rpc/index.js.map +1 -0
- package/build/src/implementations/http/express-rpc/index.test.d.ts +1 -0
- package/build/src/implementations/http/express-rpc/index.test.js +684 -0
- package/build/src/implementations/http/express-rpc/index.test.js.map +1 -0
- package/build/src/implementations/http/express-rpc/types.d.ts +11 -0
- package/build/src/implementations/http/express-rpc/types.js +2 -0
- package/build/src/implementations/http/express-rpc/types.js.map +1 -0
- package/build/src/implementations/http/hono-api/index.d.ts +102 -0
- package/build/src/implementations/http/hono-api/index.js +341 -0
- package/build/src/implementations/http/hono-api/index.js.map +1 -0
- package/build/src/implementations/http/hono-api/index.test.d.ts +1 -0
- package/build/src/implementations/http/hono-api/index.test.js +992 -0
- package/build/src/implementations/http/hono-api/index.test.js.map +1 -0
- package/build/src/implementations/http/hono-api/types.d.ts +13 -0
- package/build/src/implementations/http/hono-api/types.js +2 -0
- package/build/src/implementations/http/hono-api/types.js.map +1 -0
- package/build/src/implementations/http/hono-rpc/index.d.ts +92 -0
- package/build/src/implementations/http/hono-rpc/index.js +161 -0
- package/build/src/implementations/http/hono-rpc/index.js.map +1 -0
- package/build/src/implementations/http/hono-rpc/index.test.d.ts +1 -0
- package/build/src/implementations/http/hono-rpc/index.test.js +803 -0
- package/build/src/implementations/http/hono-rpc/index.test.js.map +1 -0
- package/build/src/implementations/http/hono-rpc/types.d.ts +11 -0
- package/build/src/implementations/http/hono-rpc/types.js +2 -0
- package/build/src/implementations/http/hono-rpc/types.js.map +1 -0
- package/build/src/implementations/http/hono-stream/index.d.ts +120 -0
- package/build/src/implementations/http/hono-stream/index.js +309 -0
- package/build/src/implementations/http/hono-stream/index.js.map +1 -0
- package/build/src/implementations/http/hono-stream/index.test.d.ts +1 -0
- package/build/src/implementations/http/hono-stream/index.test.js +1356 -0
- package/build/src/implementations/http/hono-stream/index.test.js.map +1 -0
- package/build/src/implementations/http/hono-stream/types.d.ts +15 -0
- package/build/src/implementations/http/hono-stream/types.js +2 -0
- package/build/src/implementations/http/hono-stream/types.js.map +1 -0
- package/build/src/implementations/types.d.ts +142 -0
- package/build/src/implementations/types.js +2 -0
- package/build/src/implementations/types.js.map +1 -0
- package/build/src/index.d.ts +165 -0
- package/build/src/index.js +253 -0
- package/build/src/index.js.map +1 -0
- package/build/src/index.test.d.ts +1 -0
- package/build/src/index.test.js +890 -0
- package/build/src/index.test.js.map +1 -0
- package/build/src/schema/compute-schema.d.ts +35 -0
- package/build/src/schema/compute-schema.js +41 -0
- package/build/src/schema/compute-schema.js.map +1 -0
- package/build/src/schema/compute-schema.test.d.ts +1 -0
- package/build/src/schema/compute-schema.test.js +107 -0
- package/build/src/schema/compute-schema.test.js.map +1 -0
- package/build/src/schema/extract-json-schema.d.ts +2 -0
- package/build/src/schema/extract-json-schema.js +12 -0
- package/build/src/schema/extract-json-schema.js.map +1 -0
- package/build/src/schema/extract-json-schema.test.d.ts +1 -0
- package/build/src/schema/extract-json-schema.test.js +23 -0
- package/build/src/schema/extract-json-schema.test.js.map +1 -0
- package/build/src/schema/parser.d.ts +28 -0
- package/build/src/schema/parser.js +170 -0
- package/build/src/schema/parser.js.map +1 -0
- package/build/src/schema/parser.test.d.ts +1 -0
- package/build/src/schema/parser.test.js +120 -0
- package/build/src/schema/parser.test.js.map +1 -0
- package/build/src/schema/resolve-schema-lib.d.ts +12 -0
- package/build/src/schema/resolve-schema-lib.js +11 -0
- package/build/src/schema/resolve-schema-lib.js.map +1 -0
- package/build/src/schema/resolve-schema-lib.test.d.ts +1 -0
- package/build/src/schema/resolve-schema-lib.test.js +17 -0
- package/build/src/schema/resolve-schema-lib.test.js.map +1 -0
- package/build/src/schema/types.d.ts +8 -0
- package/build/src/schema/types.js +2 -0
- package/build/src/schema/types.js.map +1 -0
- package/build/src/stack-utils.d.ts +25 -0
- package/build/src/stack-utils.js +95 -0
- package/build/src/stack-utils.js.map +1 -0
- package/build/src/stack-utils.test.d.ts +1 -0
- package/build/src/stack-utils.test.js +80 -0
- package/build/src/stack-utils.test.js.map +1 -0
- package/docs/ai-agent-setup.md +7 -6
- package/docs/core.md +5 -9
- package/docs/streaming.md +9 -9
- package/package.json +2 -13
- package/src/client/call.test.ts +162 -0
- package/src/client/errors.test.ts +43 -0
- package/src/client/fetch-adapter.test.ts +340 -0
- package/src/client/hooks.test.ts +191 -0
- package/src/client/index.test.ts +290 -0
- package/src/client/request-builder.test.ts +184 -0
- package/src/client/stream.test.ts +331 -0
- package/src/codegen/bin/cli.test.ts +260 -0
- package/src/codegen/bin/cli.ts +282 -0
- package/src/codegen/constants.ts +1 -0
- package/src/codegen/e2e.test.ts +565 -0
- package/src/codegen/emit-client-runtime.test.ts +93 -0
- package/src/codegen/emit-client-runtime.ts +114 -0
- package/src/codegen/emit-client-types.test.ts +39 -0
- package/src/codegen/emit-client-types.ts +27 -0
- package/src/codegen/emit-errors.test.ts +202 -0
- package/src/codegen/emit-errors.ts +80 -0
- package/src/codegen/emit-index.test.ts +127 -0
- package/src/codegen/emit-index.ts +58 -0
- package/src/codegen/emit-scope.test.ts +624 -0
- package/src/codegen/emit-scope.ts +389 -0
- package/src/codegen/emit-types.test.ts +205 -0
- package/src/codegen/emit-types.ts +158 -0
- package/src/codegen/group-routes.test.ts +159 -0
- package/src/codegen/group-routes.ts +61 -0
- package/src/codegen/index.ts +30 -0
- package/src/codegen/naming.test.ts +50 -0
- package/src/codegen/naming.ts +25 -0
- package/src/codegen/pipeline.test.ts +316 -0
- package/src/codegen/pipeline.ts +108 -0
- package/src/codegen/resolve-envelope.test.ts +76 -0
- package/src/codegen/resolve-envelope.ts +61 -0
- package/src/errors.test.ts +163 -0
- package/src/errors.ts +107 -0
- package/src/exports.ts +7 -0
- package/src/implementations/http/doc-registry.test.ts +415 -0
- package/src/implementations/http/doc-registry.ts +143 -0
- package/src/implementations/http/express-rpc/README.md +6 -6
- package/src/implementations/http/express-rpc/index.test.ts +957 -0
- package/src/implementations/http/express-rpc/index.ts +266 -0
- package/src/implementations/http/express-rpc/types.ts +16 -0
- package/src/implementations/http/hono-api/index.test.ts +1341 -0
- package/src/implementations/http/hono-api/index.ts +463 -0
- package/src/implementations/http/hono-api/types.ts +16 -0
- package/src/implementations/http/hono-rpc/README.md +6 -6
- package/src/implementations/http/hono-rpc/index.test.ts +1075 -0
- package/src/implementations/http/hono-rpc/index.ts +238 -0
- package/src/implementations/http/hono-rpc/types.ts +16 -0
- package/src/implementations/http/hono-stream/README.md +12 -12
- package/src/implementations/http/hono-stream/index.test.ts +1768 -0
- package/src/implementations/http/hono-stream/index.ts +456 -0
- package/src/implementations/http/hono-stream/types.ts +20 -0
- package/src/implementations/types.ts +174 -0
- package/src/index.test.ts +1185 -0
- package/src/index.ts +522 -0
- package/src/schema/compute-schema.test.ts +128 -0
- package/src/schema/compute-schema.ts +88 -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 +182 -0
- package/src/schema/parser.ts +215 -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 +20 -0
- package/src/stack-utils.test.ts +94 -0
- package/src/stack-utils.ts +129 -0
- package/agent_config/claude-code/skills/review/SKILL.md +0 -53
- package/docs/superpowers/plans/2026-03-30-client-codegen.md +0 -2833
- package/docs/superpowers/specs/2026-03-30-client-codegen-design.md +0 -632
- /package/agent_config/claude-code/skills/{guide → ts-procedures}/patterns.md +0 -0
- /package/agent_config/claude-code/skills/{review → ts-procedures-review}/checklist.md +0 -0
- /package/agent_config/claude-code/skills/{scaffold → ts-procedures-scaffold}/templates/express-rpc.md +0 -0
- /package/agent_config/claude-code/skills/{scaffold → ts-procedures-scaffold}/templates/hono-api.md +0 -0
- /package/agent_config/claude-code/skills/{scaffold → ts-procedures-scaffold}/templates/hono-rpc.md +0 -0
- /package/agent_config/claude-code/skills/{scaffold → ts-procedures-scaffold}/templates/hono-stream.md +0 -0
- /package/agent_config/claude-code/skills/{scaffold → ts-procedures-scaffold}/templates/procedure.md +0 -0
- /package/agent_config/claude-code/skills/{scaffold → ts-procedures-scaffold}/templates/stream-procedure.md +0 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
ProcedureError,
|
|
4
|
+
ProcedureValidationError,
|
|
5
|
+
ProcedureRegistrationError,
|
|
6
|
+
} from './errors.js'
|
|
7
|
+
import { DefinitionInfo } from './stack-utils.js'
|
|
8
|
+
|
|
9
|
+
describe('Error Classes', () => {
|
|
10
|
+
test('ProcedureError has correct properties', () => {
|
|
11
|
+
const err = new ProcedureError('TestProc', 'Something failed', { code: 123 })
|
|
12
|
+
|
|
13
|
+
expect(err.name).toBe('ProcedureError')
|
|
14
|
+
expect(err.procedureName).toBe('TestProc')
|
|
15
|
+
expect(err.message).toBe('Something failed')
|
|
16
|
+
expect(err.meta).toEqual({ code: 123 })
|
|
17
|
+
expect(err instanceof Error).toBe(true)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
test('ProcedureValidationError extends ProcedureError', () => {
|
|
21
|
+
const err = new ProcedureValidationError('TestProc', 'Validation failed', [])
|
|
22
|
+
|
|
23
|
+
expect(err instanceof ProcedureError).toBe(true)
|
|
24
|
+
expect(err instanceof Error).toBe(true)
|
|
25
|
+
expect(err.name).toBe('ProcedureValidationError')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test('ProcedureRegistrationError extends ProcedureError', () => {
|
|
29
|
+
const err = new ProcedureRegistrationError('TestProc', 'Registration failed')
|
|
30
|
+
|
|
31
|
+
expect(err instanceof ProcedureError).toBe(true)
|
|
32
|
+
expect(err instanceof Error).toBe(true)
|
|
33
|
+
expect(err.name).toBe('ProcedureRegistrationError')
|
|
34
|
+
expect(err.procedureName).toBe('TestProc')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('ProcedureError supports cause property', () => {
|
|
38
|
+
const cause = new Error('Original error')
|
|
39
|
+
const err = new ProcedureError('TestProc', 'Wrapped error')
|
|
40
|
+
err.cause = cause
|
|
41
|
+
|
|
42
|
+
expect(err.cause).toBe(cause)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('All error types have consistent procedureName property', () => {
|
|
46
|
+
const baseErr = new ProcedureError('Proc1', 'message')
|
|
47
|
+
const validationErr = new ProcedureValidationError('Proc2', 'message', [])
|
|
48
|
+
const registrationErr = new ProcedureRegistrationError('Proc3', 'message')
|
|
49
|
+
|
|
50
|
+
expect(baseErr.procedureName).toBe('Proc1')
|
|
51
|
+
expect(validationErr.procedureName).toBe('Proc2')
|
|
52
|
+
expect(registrationErr.procedureName).toBe('Proc3')
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
describe('Error Classes - Definition Info', () => {
|
|
57
|
+
const mockDefinitionInfo: DefinitionInfo = {
|
|
58
|
+
definedAt: {
|
|
59
|
+
file: '/app/procedures/user.ts',
|
|
60
|
+
line: 25,
|
|
61
|
+
column: 3,
|
|
62
|
+
raw: 'at Object.<anonymous> (/app/procedures/user.ts:25:3)',
|
|
63
|
+
},
|
|
64
|
+
definitionStack: 'Error\n at Object.<anonymous> (/app/procedures/user.ts:25:3)',
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
test('ProcedureError includes definedAt when provided', () => {
|
|
68
|
+
const err = new ProcedureError('TestProc', 'Something failed', undefined, mockDefinitionInfo)
|
|
69
|
+
|
|
70
|
+
expect(err.definedAt).toBeDefined()
|
|
71
|
+
expect(err.definedAt?.file).toBe('/app/procedures/user.ts')
|
|
72
|
+
expect(err.definedAt?.line).toBe(25)
|
|
73
|
+
expect(err.definedAt?.column).toBe(3)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('ProcedureError includes definitionStack when provided', () => {
|
|
77
|
+
const err = new ProcedureError('TestProc', 'Something failed', undefined, mockDefinitionInfo)
|
|
78
|
+
|
|
79
|
+
expect(err.definitionStack).toBeDefined()
|
|
80
|
+
expect(err.definitionStack).toContain('/app/procedures/user.ts')
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test('ProcedureError works without definitionInfo (backward compat)', () => {
|
|
84
|
+
const err = new ProcedureError('TestProc', 'Something failed', { code: 123 })
|
|
85
|
+
|
|
86
|
+
expect(err.definedAt).toBeUndefined()
|
|
87
|
+
expect(err.definitionStack).toBeUndefined()
|
|
88
|
+
expect(err.procedureName).toBe('TestProc')
|
|
89
|
+
expect(err.message).toBe('Something failed')
|
|
90
|
+
expect(err.meta).toEqual({ code: 123 })
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
test('ProcedureValidationError includes definedAt when provided', () => {
|
|
94
|
+
const err = new ProcedureValidationError('TestProc', 'Validation failed', [], mockDefinitionInfo)
|
|
95
|
+
|
|
96
|
+
expect(err.definedAt).toBeDefined()
|
|
97
|
+
expect(err.definedAt?.file).toBe('/app/procedures/user.ts')
|
|
98
|
+
expect(err.definedAt?.line).toBe(25)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test('ProcedureValidationError works without definitionInfo (backward compat)', () => {
|
|
102
|
+
const err = new ProcedureValidationError('TestProc', 'Validation failed', [])
|
|
103
|
+
|
|
104
|
+
expect(err.definedAt).toBeUndefined()
|
|
105
|
+
expect(err.definitionStack).toBeUndefined()
|
|
106
|
+
expect(err.name).toBe('ProcedureValidationError')
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test('ProcedureRegistrationError includes definedAt when provided', () => {
|
|
110
|
+
const err = new ProcedureRegistrationError('TestProc', 'Registration failed', mockDefinitionInfo)
|
|
111
|
+
|
|
112
|
+
expect(err.definedAt).toBeDefined()
|
|
113
|
+
expect(err.definedAt?.file).toBe('/app/procedures/user.ts')
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
test('ProcedureRegistrationError works without definitionInfo (backward compat)', () => {
|
|
117
|
+
const err = new ProcedureRegistrationError('TestProc', 'Registration failed')
|
|
118
|
+
|
|
119
|
+
expect(err.definedAt).toBeUndefined()
|
|
120
|
+
expect(err.definitionStack).toBeUndefined()
|
|
121
|
+
expect(err.name).toBe('ProcedureRegistrationError')
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
test('getDefinitionLocation returns formatted location string', () => {
|
|
125
|
+
const err = new ProcedureError('TestProc', 'Something failed', undefined, mockDefinitionInfo)
|
|
126
|
+
|
|
127
|
+
const location = err.getDefinitionLocation()
|
|
128
|
+
|
|
129
|
+
expect(location).toBe('/app/procedures/user.ts:25:3')
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
test('getDefinitionLocation returns undefined when no definedAt', () => {
|
|
133
|
+
const err = new ProcedureError('TestProc', 'Something failed')
|
|
134
|
+
|
|
135
|
+
const location = err.getDefinitionLocation()
|
|
136
|
+
|
|
137
|
+
expect(location).toBeUndefined()
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
test('enhanced stack contains definition location', () => {
|
|
141
|
+
const err = new ProcedureError('TestProc', 'Something failed', undefined, mockDefinitionInfo)
|
|
142
|
+
|
|
143
|
+
expect(err.stack).toContain('--- Procedure "TestProc" defined at ---')
|
|
144
|
+
expect(err.stack).toContain('/app/procedures/user.ts:25:3')
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
test('stack is not modified when no definitionInfo', () => {
|
|
148
|
+
const err = new ProcedureError('TestProc', 'Something failed')
|
|
149
|
+
|
|
150
|
+
expect(err.stack).toBeDefined()
|
|
151
|
+
expect(err.stack).not.toContain('--- Procedure')
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
test('all error types enhance stack with definition location', () => {
|
|
155
|
+
const baseErr = new ProcedureError('Proc1', 'message', undefined, mockDefinitionInfo)
|
|
156
|
+
const validationErr = new ProcedureValidationError('Proc2', 'message', [], mockDefinitionInfo)
|
|
157
|
+
const registrationErr = new ProcedureRegistrationError('Proc3', 'message', mockDefinitionInfo)
|
|
158
|
+
|
|
159
|
+
expect(baseErr.stack).toContain('--- Procedure "Proc1" defined at ---')
|
|
160
|
+
expect(validationErr.stack).toContain('--- Procedure "Proc2" defined at ---')
|
|
161
|
+
expect(registrationErr.stack).toContain('--- Procedure "Proc3" defined at ---')
|
|
162
|
+
})
|
|
163
|
+
})
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { TSchemaValidationError } from './schema/parser.js'
|
|
2
|
+
import { DefinitionInfo, DefinitionLocation, formatDefinitionInfo } from './stack-utils.js'
|
|
3
|
+
import { kebabCase } from 'es-toolkit/string'
|
|
4
|
+
|
|
5
|
+
export class ProcedureError extends Error {
|
|
6
|
+
cause?: unknown
|
|
7
|
+
readonly definedAt?: DefinitionLocation
|
|
8
|
+
readonly definitionStack?: string
|
|
9
|
+
|
|
10
|
+
constructor(
|
|
11
|
+
readonly procedureName: string,
|
|
12
|
+
readonly message: string,
|
|
13
|
+
readonly meta?: object,
|
|
14
|
+
// Used for error stack trace details
|
|
15
|
+
definitionInfo?: DefinitionInfo
|
|
16
|
+
) {
|
|
17
|
+
super(message)
|
|
18
|
+
this.name = 'ProcedureError'
|
|
19
|
+
|
|
20
|
+
if (definitionInfo) {
|
|
21
|
+
this.definedAt = definitionInfo.definedAt
|
|
22
|
+
this.definitionStack = definitionInfo.definitionStack
|
|
23
|
+
this.enhanceStack()
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// https://www.dannyguo.com/blog/how-to-fix-instanceof-not-working-for-custom-errors-in-typescript/
|
|
27
|
+
Object.setPrototypeOf(this, ProcedureError.prototype)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Returns a formatted string showing where the procedure was defined.
|
|
32
|
+
*/
|
|
33
|
+
getDefinitionLocation(): string | undefined {
|
|
34
|
+
if (!this.definedAt) {
|
|
35
|
+
return undefined
|
|
36
|
+
}
|
|
37
|
+
return `${this.definedAt.file}:${this.definedAt.line}:${this.definedAt.column}`
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Enhances the error stack with definition location information.
|
|
42
|
+
*/
|
|
43
|
+
private enhanceStack(): void {
|
|
44
|
+
if (!this.stack || !this.definedAt) {
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const definitionSection = formatDefinitionInfo(
|
|
49
|
+
{ definedAt: this.definedAt, definitionStack: this.definitionStack },
|
|
50
|
+
this.procedureName
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
if (definitionSection) {
|
|
54
|
+
this.stack = this.stack + definitionSection
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export class ProcedureValidationError extends ProcedureError {
|
|
60
|
+
constructor(
|
|
61
|
+
readonly procedureName: string,
|
|
62
|
+
message: string,
|
|
63
|
+
readonly errors?: TSchemaValidationError[],
|
|
64
|
+
// Used for error stack trace details
|
|
65
|
+
definitionInfo?: DefinitionInfo
|
|
66
|
+
) {
|
|
67
|
+
const readableErrors = errors
|
|
68
|
+
?.map((err) => `- ${kebabCase(err.instancePath).replace('-', '.')} ${err.message}`)
|
|
69
|
+
.join(', ')
|
|
70
|
+
super(procedureName, message + ' ' + readableErrors, { errors }, definitionInfo)
|
|
71
|
+
this.name = 'ProcedureValidationError'
|
|
72
|
+
|
|
73
|
+
// https://www.dannyguo.com/blog/how-to-fix-instanceof-not-working-for-custom-errors-in-typescript/
|
|
74
|
+
Object.setPrototypeOf(this, ProcedureValidationError.prototype)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export class ProcedureRegistrationError extends ProcedureError {
|
|
79
|
+
constructor(
|
|
80
|
+
readonly procedureName: string,
|
|
81
|
+
message: string,
|
|
82
|
+
// Used for error stack trace details
|
|
83
|
+
definitionInfo?: DefinitionInfo
|
|
84
|
+
) {
|
|
85
|
+
super(procedureName, message, undefined, definitionInfo)
|
|
86
|
+
this.name = 'ProcedureRegistrationError'
|
|
87
|
+
|
|
88
|
+
// https://www.dannyguo.com/blog/how-to-fix-instanceof-not-working-for-custom-errors-in-typescript/
|
|
89
|
+
Object.setPrototypeOf(this, ProcedureRegistrationError.prototype)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export class ProcedureYieldValidationError extends ProcedureError {
|
|
94
|
+
constructor(
|
|
95
|
+
readonly procedureName: string,
|
|
96
|
+
message: string,
|
|
97
|
+
readonly errors?: TSchemaValidationError[],
|
|
98
|
+
// Used for error stack trace details
|
|
99
|
+
definitionInfo?: DefinitionInfo
|
|
100
|
+
) {
|
|
101
|
+
super(procedureName, message, undefined, definitionInfo)
|
|
102
|
+
this.name = 'ProcedureYieldValidationError'
|
|
103
|
+
|
|
104
|
+
// https://www.dannyguo.com/blog/how-to-fix-instanceof-not-working-for-custom-errors-in-typescript/
|
|
105
|
+
Object.setPrototypeOf(this, ProcedureYieldValidationError.prototype)
|
|
106
|
+
}
|
|
107
|
+
}
|
package/src/exports.ts
ADDED
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
import { describe, expect, it, test } from 'vitest'
|
|
2
|
+
import { v } from 'suretype'
|
|
3
|
+
import { Procedures } from '../../index.js'
|
|
4
|
+
import { HonoRPCAppBuilder } from './hono-rpc/index.js'
|
|
5
|
+
import { DocRegistry } from './doc-registry.js'
|
|
6
|
+
import type {
|
|
7
|
+
AnyHttpRouteDoc,
|
|
8
|
+
RPCHttpRouteDoc,
|
|
9
|
+
RPCConfig,
|
|
10
|
+
APIHttpRouteDoc,
|
|
11
|
+
StreamHttpRouteDoc,
|
|
12
|
+
DocSource,
|
|
13
|
+
DocEnvelope,
|
|
14
|
+
} from '../types.js'
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Helpers — minimal doc fixtures
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
const rpcDoc: RPCHttpRouteDoc = {
|
|
21
|
+
kind: 'rpc',
|
|
22
|
+
name: 'Echo',
|
|
23
|
+
path: '/echo/1',
|
|
24
|
+
method: 'post',
|
|
25
|
+
scope: 'echo',
|
|
26
|
+
version: 1,
|
|
27
|
+
jsonSchema: { body: { type: 'object' } },
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const apiDoc: APIHttpRouteDoc = {
|
|
31
|
+
kind: 'api',
|
|
32
|
+
name: 'GetUser',
|
|
33
|
+
path: '/users/:id',
|
|
34
|
+
method: 'get',
|
|
35
|
+
fullPath: '/api/users/:id',
|
|
36
|
+
jsonSchema: { pathParams: { type: 'object' } },
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const streamDoc: StreamHttpRouteDoc = {
|
|
40
|
+
kind: 'stream',
|
|
41
|
+
name: 'Feed',
|
|
42
|
+
path: '/feed/1',
|
|
43
|
+
methods: ['get'],
|
|
44
|
+
streamMode: 'sse',
|
|
45
|
+
scope: 'feed',
|
|
46
|
+
version: 1,
|
|
47
|
+
jsonSchema: { yieldType: { type: 'object' } },
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function makeSource<T extends AnyHttpRouteDoc>(docs: T[]): DocSource<T> {
|
|
51
|
+
return { docs }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// Tests
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
describe('DocRegistry', () => {
|
|
59
|
+
// --------------------------------------------------------------------------
|
|
60
|
+
// Constructor
|
|
61
|
+
// --------------------------------------------------------------------------
|
|
62
|
+
describe('constructor', () => {
|
|
63
|
+
test('uses defaults when no config provided', () => {
|
|
64
|
+
const registry = new DocRegistry()
|
|
65
|
+
const out = registry.toJSON()
|
|
66
|
+
expect(out.basePath).toBe('')
|
|
67
|
+
expect(out.headers).toEqual([])
|
|
68
|
+
expect(out.errors).toEqual([])
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test('accepts partial config', () => {
|
|
72
|
+
const registry = new DocRegistry({ basePath: '/v1' })
|
|
73
|
+
const out = registry.toJSON()
|
|
74
|
+
expect(out.basePath).toBe('/v1')
|
|
75
|
+
expect(out.headers).toEqual([])
|
|
76
|
+
expect(out.errors).toEqual([])
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
test('accepts full config', () => {
|
|
80
|
+
const headers = [{ name: 'Authorization', description: 'Bearer token', required: true }]
|
|
81
|
+
const errors = [{ name: 'Unauthorized', statusCode: 401, description: 'Missing token' }]
|
|
82
|
+
const registry = new DocRegistry({ basePath: '/api', headers, errors })
|
|
83
|
+
const out = registry.toJSON()
|
|
84
|
+
expect(out.basePath).toBe('/api')
|
|
85
|
+
expect(out.headers).toEqual(headers)
|
|
86
|
+
expect(out.errors).toEqual(errors)
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
// --------------------------------------------------------------------------
|
|
91
|
+
// from()
|
|
92
|
+
// --------------------------------------------------------------------------
|
|
93
|
+
describe('from()', () => {
|
|
94
|
+
test('returns this for chaining', () => {
|
|
95
|
+
const registry = new DocRegistry()
|
|
96
|
+
const result = registry.from(makeSource([rpcDoc]))
|
|
97
|
+
expect(result).toBe(registry)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
test('accepts RPC builder source', () => {
|
|
101
|
+
const registry = new DocRegistry().from(makeSource([rpcDoc]))
|
|
102
|
+
expect(registry.toJSON().routes).toEqual([rpcDoc])
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
test('accepts API builder source', () => {
|
|
106
|
+
const registry = new DocRegistry().from(makeSource([apiDoc]))
|
|
107
|
+
expect(registry.toJSON().routes).toEqual([apiDoc])
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
test('accepts Stream builder source', () => {
|
|
111
|
+
const registry = new DocRegistry().from(makeSource([streamDoc]))
|
|
112
|
+
expect(registry.toJSON().routes).toEqual([streamDoc])
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
test('accepts plain { docs } object', () => {
|
|
116
|
+
const plain = { docs: [rpcDoc, apiDoc] }
|
|
117
|
+
const registry = new DocRegistry().from(plain)
|
|
118
|
+
expect(registry.toJSON().routes).toHaveLength(2)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
test('builder not yet built returns empty routes', () => {
|
|
122
|
+
const emptySource = makeSource<RPCHttpRouteDoc>([])
|
|
123
|
+
const registry = new DocRegistry().from(emptySource)
|
|
124
|
+
expect(registry.toJSON().routes).toEqual([])
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
test('same builder twice duplicates routes', () => {
|
|
128
|
+
const source = makeSource([rpcDoc])
|
|
129
|
+
const registry = new DocRegistry().from(source).from(source)
|
|
130
|
+
expect(registry.toJSON().routes).toHaveLength(2)
|
|
131
|
+
expect(registry.toJSON().routes[0]).toEqual(registry.toJSON().routes[1])
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
test('reads docs lazily at toJSON() time', () => {
|
|
135
|
+
const mutableDocs: RPCHttpRouteDoc[] = []
|
|
136
|
+
const source: DocSource<RPCHttpRouteDoc> = { get docs() { return mutableDocs } }
|
|
137
|
+
const registry = new DocRegistry().from(source)
|
|
138
|
+
|
|
139
|
+
expect(registry.toJSON().routes).toHaveLength(0)
|
|
140
|
+
|
|
141
|
+
mutableDocs.push(rpcDoc)
|
|
142
|
+
expect(registry.toJSON().routes).toHaveLength(1)
|
|
143
|
+
})
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
// --------------------------------------------------------------------------
|
|
147
|
+
// toJSON()
|
|
148
|
+
// --------------------------------------------------------------------------
|
|
149
|
+
describe('toJSON()', () => {
|
|
150
|
+
test('returns correct DocEnvelope shape', () => {
|
|
151
|
+
const registry = new DocRegistry({ basePath: '/api' })
|
|
152
|
+
const out = registry.toJSON()
|
|
153
|
+
expect(out).toHaveProperty('basePath')
|
|
154
|
+
expect(out).toHaveProperty('headers')
|
|
155
|
+
expect(out).toHaveProperty('errors')
|
|
156
|
+
expect(out).toHaveProperty('routes')
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
test('collects routes from all sources', () => {
|
|
160
|
+
const registry = new DocRegistry()
|
|
161
|
+
.from(makeSource([rpcDoc]))
|
|
162
|
+
.from(makeSource([apiDoc]))
|
|
163
|
+
.from(makeSource([streamDoc]))
|
|
164
|
+
|
|
165
|
+
const out = registry.toJSON()
|
|
166
|
+
expect(out.routes).toHaveLength(3)
|
|
167
|
+
expect(out.routes).toEqual([rpcDoc, apiDoc, streamDoc])
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
test('empty when no sources', () => {
|
|
171
|
+
const out = new DocRegistry().toJSON()
|
|
172
|
+
expect(out.routes).toEqual([])
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
test('headers and errors are copies', () => {
|
|
176
|
+
const headers = [{ name: 'X-Custom' }]
|
|
177
|
+
const errors = [{ name: 'E', statusCode: 500, description: 'd' }]
|
|
178
|
+
const registry = new DocRegistry({ headers, errors })
|
|
179
|
+
const out = registry.toJSON()
|
|
180
|
+
expect(out.headers).toEqual(headers)
|
|
181
|
+
expect(out.headers).not.toBe(headers)
|
|
182
|
+
expect(out.errors).toEqual(errors)
|
|
183
|
+
expect(out.errors).not.toBe(errors)
|
|
184
|
+
})
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
// --------------------------------------------------------------------------
|
|
188
|
+
// toJSON() filter
|
|
189
|
+
// --------------------------------------------------------------------------
|
|
190
|
+
describe('toJSON() filter', () => {
|
|
191
|
+
test('excludes routes by name', () => {
|
|
192
|
+
const registry = new DocRegistry()
|
|
193
|
+
.from(makeSource([rpcDoc]))
|
|
194
|
+
.from(makeSource([apiDoc]))
|
|
195
|
+
|
|
196
|
+
const out = registry.toJSON({ filter: (r) => r.name !== 'Echo' })
|
|
197
|
+
expect(out.routes).toHaveLength(1)
|
|
198
|
+
expect(out.routes[0]!.name).toBe('GetUser')
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
test('excludes routes by type field', () => {
|
|
202
|
+
const registry = new DocRegistry()
|
|
203
|
+
.from(makeSource([rpcDoc]))
|
|
204
|
+
.from(makeSource([apiDoc]))
|
|
205
|
+
|
|
206
|
+
const out = registry.toJSON({
|
|
207
|
+
filter: (r) => 'method' in r && (r as RPCHttpRouteDoc).method === 'post' && 'scope' in r,
|
|
208
|
+
})
|
|
209
|
+
expect(out.routes).toHaveLength(1)
|
|
210
|
+
expect(out.routes[0]!.name).toBe('Echo')
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
test('all filtered returns empty routes array', () => {
|
|
214
|
+
const registry = new DocRegistry().from(makeSource([rpcDoc]))
|
|
215
|
+
const out = registry.toJSON({ filter: () => false })
|
|
216
|
+
expect(out.routes).toEqual([])
|
|
217
|
+
})
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
// --------------------------------------------------------------------------
|
|
221
|
+
// toJSON() transform
|
|
222
|
+
// --------------------------------------------------------------------------
|
|
223
|
+
describe('toJSON() transform', () => {
|
|
224
|
+
test('adds fields', () => {
|
|
225
|
+
const registry = new DocRegistry({ basePath: '/api' }).from(makeSource([rpcDoc]))
|
|
226
|
+
const out = registry.toJSON({
|
|
227
|
+
transform: (envelope) => ({ ...envelope, version: '2.0' }),
|
|
228
|
+
})
|
|
229
|
+
expect(out.version).toBe('2.0')
|
|
230
|
+
expect(out.basePath).toBe('/api')
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
test('reshapes envelope', () => {
|
|
234
|
+
const registry = new DocRegistry().from(makeSource([rpcDoc]))
|
|
235
|
+
const out = registry.toJSON({
|
|
236
|
+
transform: (envelope) => ({ count: envelope.routes.length }),
|
|
237
|
+
})
|
|
238
|
+
expect(out).toEqual({ count: 1 })
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
test('runs after filter', () => {
|
|
242
|
+
const registry = new DocRegistry()
|
|
243
|
+
.from(makeSource([rpcDoc]))
|
|
244
|
+
.from(makeSource([apiDoc]))
|
|
245
|
+
|
|
246
|
+
const out = registry.toJSON({
|
|
247
|
+
filter: (r) => r.name === 'Echo',
|
|
248
|
+
transform: (envelope) => ({ ...envelope, filteredCount: envelope.routes.length }),
|
|
249
|
+
})
|
|
250
|
+
expect(out.filteredCount).toBe(1)
|
|
251
|
+
expect(out.routes).toHaveLength(1)
|
|
252
|
+
})
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
// --------------------------------------------------------------------------
|
|
256
|
+
// defaultErrors()
|
|
257
|
+
// --------------------------------------------------------------------------
|
|
258
|
+
describe('defaultErrors()', () => {
|
|
259
|
+
test('returns 4 entries', () => {
|
|
260
|
+
const errors = DocRegistry.defaultErrors()
|
|
261
|
+
expect(errors).toHaveLength(4)
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
test('has correct error names', () => {
|
|
265
|
+
const names = DocRegistry.defaultErrors().map((e) => e.name)
|
|
266
|
+
expect(names).toEqual([
|
|
267
|
+
'ProcedureError',
|
|
268
|
+
'ProcedureValidationError',
|
|
269
|
+
'ProcedureYieldValidationError',
|
|
270
|
+
'ProcedureRegistrationError',
|
|
271
|
+
])
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
test('has correct status codes', () => {
|
|
275
|
+
const errors = DocRegistry.defaultErrors()
|
|
276
|
+
expect(errors[0]!.statusCode).toBe(500) // ProcedureError
|
|
277
|
+
expect(errors[1]!.statusCode).toBe(400) // ProcedureValidationError
|
|
278
|
+
expect(errors[2]!.statusCode).toBe(500) // ProcedureYieldValidationError
|
|
279
|
+
expect(errors[3]!.statusCode).toBe(500) // ProcedureRegistrationError
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
test('each entry has schema', () => {
|
|
283
|
+
for (const err of DocRegistry.defaultErrors()) {
|
|
284
|
+
expect(err.schema).toBeDefined()
|
|
285
|
+
expect(err.schema!.type).toBe('object')
|
|
286
|
+
}
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
test('returns a new array each call', () => {
|
|
290
|
+
const a = DocRegistry.defaultErrors()
|
|
291
|
+
const b = DocRegistry.defaultErrors()
|
|
292
|
+
expect(a).not.toBe(b)
|
|
293
|
+
expect(a).toEqual(b)
|
|
294
|
+
})
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
// --------------------------------------------------------------------------
|
|
298
|
+
// kind discriminant
|
|
299
|
+
// --------------------------------------------------------------------------
|
|
300
|
+
describe('kind discriminant', () => {
|
|
301
|
+
it('rpcDoc fixture requires kind field', () => {
|
|
302
|
+
const doc = rpcDoc
|
|
303
|
+
expect(doc.kind).toBe('rpc')
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
it('apiDoc fixture requires kind field', () => {
|
|
307
|
+
const doc = apiDoc
|
|
308
|
+
expect(doc.kind).toBe('api')
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
it('streamDoc fixture requires kind field', () => {
|
|
312
|
+
const doc = streamDoc
|
|
313
|
+
expect(doc.kind).toBe('stream')
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
it('narrows AnyHttpRouteDoc union via kind', () => {
|
|
317
|
+
const routes: AnyHttpRouteDoc[] = [rpcDoc, apiDoc, streamDoc]
|
|
318
|
+
for (const route of routes) {
|
|
319
|
+
expect(route.kind).toBeDefined()
|
|
320
|
+
}
|
|
321
|
+
})
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
// --------------------------------------------------------------------------
|
|
325
|
+
// Integration
|
|
326
|
+
// --------------------------------------------------------------------------
|
|
327
|
+
describe('integration', () => {
|
|
328
|
+
test('multiple builder types composed', () => {
|
|
329
|
+
const registry = new DocRegistry({
|
|
330
|
+
basePath: '/api',
|
|
331
|
+
headers: [{ name: 'Authorization', description: 'Bearer token', required: false }],
|
|
332
|
+
errors: DocRegistry.defaultErrors(),
|
|
333
|
+
})
|
|
334
|
+
.from(makeSource([rpcDoc]))
|
|
335
|
+
.from(makeSource([apiDoc]))
|
|
336
|
+
.from(makeSource([streamDoc]))
|
|
337
|
+
|
|
338
|
+
const out = registry.toJSON()
|
|
339
|
+
expect(out.basePath).toBe('/api')
|
|
340
|
+
expect(out.headers).toHaveLength(1)
|
|
341
|
+
expect(out.errors).toHaveLength(4)
|
|
342
|
+
expect(out.routes).toHaveLength(3)
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
test('filter + transform combined', () => {
|
|
346
|
+
const registry = new DocRegistry({
|
|
347
|
+
basePath: '/api',
|
|
348
|
+
errors: DocRegistry.defaultErrors(),
|
|
349
|
+
})
|
|
350
|
+
.from(makeSource([rpcDoc]))
|
|
351
|
+
.from(makeSource([apiDoc]))
|
|
352
|
+
.from(makeSource([streamDoc]))
|
|
353
|
+
|
|
354
|
+
const out = registry.toJSON({
|
|
355
|
+
filter: (r) => r.name !== 'Feed',
|
|
356
|
+
transform: (envelope) => ({
|
|
357
|
+
...envelope,
|
|
358
|
+
generatedAt: '2026-01-01',
|
|
359
|
+
}),
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
expect(out.routes).toHaveLength(2)
|
|
363
|
+
expect(out.routes.map((r: AnyHttpRouteDoc) => r.name)).toEqual(['Echo', 'GetUser'])
|
|
364
|
+
expect(out.generatedAt).toBe('2026-01-01')
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
test('output is JSON.stringify-safe', () => {
|
|
368
|
+
const registry = new DocRegistry({
|
|
369
|
+
basePath: '/api',
|
|
370
|
+
headers: [{ name: 'X-Request-Id', example: 'abc-123' }],
|
|
371
|
+
errors: DocRegistry.defaultErrors(),
|
|
372
|
+
})
|
|
373
|
+
.from(makeSource([rpcDoc]))
|
|
374
|
+
.from(makeSource([apiDoc]))
|
|
375
|
+
|
|
376
|
+
const out = registry.toJSON()
|
|
377
|
+
const str = JSON.stringify(out)
|
|
378
|
+
expect(() => JSON.parse(str)).not.toThrow()
|
|
379
|
+
expect(JSON.parse(str)).toEqual(out)
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
test('JSON.stringify(registry) produces default envelope', () => {
|
|
383
|
+
const registry = new DocRegistry({ basePath: '/api' })
|
|
384
|
+
.from(makeSource([rpcDoc]))
|
|
385
|
+
|
|
386
|
+
const str = JSON.stringify(registry)
|
|
387
|
+
const parsed = JSON.parse(str)
|
|
388
|
+
expect(parsed.basePath).toBe('/api')
|
|
389
|
+
expect(parsed.routes).toHaveLength(1)
|
|
390
|
+
expect(parsed.routes[0].name).toBe('Echo')
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
test('accepts a real HonoRPCAppBuilder as source', () => {
|
|
394
|
+
const RPC = Procedures<{ userId: string }, RPCConfig>()
|
|
395
|
+
|
|
396
|
+
RPC.Create(
|
|
397
|
+
'Ping',
|
|
398
|
+
{ scope: 'ping', version: 1, schema: { params: v.object({ msg: v.string() }) } },
|
|
399
|
+
async (_ctx, params) => params,
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
const builder = new HonoRPCAppBuilder()
|
|
403
|
+
builder.register(RPC, () => ({ userId: '123' }))
|
|
404
|
+
builder.build()
|
|
405
|
+
|
|
406
|
+
const registry = new DocRegistry({ basePath: '/rpc' }).from(builder)
|
|
407
|
+
const out = registry.toJSON()
|
|
408
|
+
|
|
409
|
+
expect(out.basePath).toBe('/rpc')
|
|
410
|
+
expect(out.routes).toHaveLength(1)
|
|
411
|
+
expect(out.routes[0]!.name).toBe('Ping')
|
|
412
|
+
expect(out.routes[0]!).toHaveProperty('jsonSchema')
|
|
413
|
+
})
|
|
414
|
+
})
|
|
415
|
+
})
|