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.
Files changed (305) hide show
  1. package/README.md +1 -1
  2. package/agent_config/bin/postinstall.mjs +3 -3
  3. package/agent_config/bin/setup.mjs +22 -11
  4. package/agent_config/claude-code/agents/ts-procedures-architect.md +46 -101
  5. package/agent_config/claude-code/skills/{guide → ts-procedures}/SKILL.md +50 -35
  6. package/agent_config/claude-code/skills/{guide → ts-procedures}/anti-patterns.md +6 -5
  7. package/agent_config/claude-code/skills/{guide → ts-procedures}/api-reference.md +60 -49
  8. package/agent_config/claude-code/skills/ts-procedures-review/SKILL.md +48 -0
  9. package/agent_config/claude-code/skills/{scaffold → ts-procedures-scaffold}/SKILL.md +19 -24
  10. package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/client.md +115 -0
  11. package/agent_config/lib/install-claude.mjs +35 -87
  12. package/build/src/client/call.d.ts +14 -0
  13. package/build/src/client/call.js +47 -0
  14. package/build/src/client/call.js.map +1 -0
  15. package/build/src/client/call.test.d.ts +1 -0
  16. package/build/src/client/call.test.js +124 -0
  17. package/build/src/client/call.test.js.map +1 -0
  18. package/build/src/client/errors.d.ts +25 -0
  19. package/build/src/client/errors.js +33 -0
  20. package/build/src/client/errors.js.map +1 -0
  21. package/build/src/client/errors.test.d.ts +1 -0
  22. package/build/src/client/errors.test.js +41 -0
  23. package/build/src/client/errors.test.js.map +1 -0
  24. package/build/src/client/fetch-adapter.d.ts +12 -0
  25. package/build/src/client/fetch-adapter.js +156 -0
  26. package/build/src/client/fetch-adapter.js.map +1 -0
  27. package/build/src/client/fetch-adapter.test.d.ts +1 -0
  28. package/build/src/client/fetch-adapter.test.js +271 -0
  29. package/build/src/client/fetch-adapter.test.js.map +1 -0
  30. package/build/src/client/hooks.d.ts +17 -0
  31. package/build/src/client/hooks.js +40 -0
  32. package/build/src/client/hooks.js.map +1 -0
  33. package/build/src/client/hooks.test.d.ts +1 -0
  34. package/build/src/client/hooks.test.js +163 -0
  35. package/build/src/client/hooks.test.js.map +1 -0
  36. package/build/src/client/index.d.ts +22 -0
  37. package/build/src/client/index.js +67 -0
  38. package/build/src/client/index.js.map +1 -0
  39. package/build/src/client/index.test.d.ts +1 -0
  40. package/build/src/client/index.test.js +231 -0
  41. package/build/src/client/index.test.js.map +1 -0
  42. package/build/src/client/request-builder.d.ts +13 -0
  43. package/build/src/client/request-builder.js +53 -0
  44. package/build/src/client/request-builder.js.map +1 -0
  45. package/build/src/client/request-builder.test.d.ts +1 -0
  46. package/build/src/client/request-builder.test.js +160 -0
  47. package/build/src/client/request-builder.test.js.map +1 -0
  48. package/build/src/client/stream.d.ts +27 -0
  49. package/build/src/client/stream.js +118 -0
  50. package/build/src/client/stream.js.map +1 -0
  51. package/build/src/client/stream.test.d.ts +1 -0
  52. package/build/src/client/stream.test.js +228 -0
  53. package/build/src/client/stream.test.js.map +1 -0
  54. package/build/src/client/types.d.ts +78 -0
  55. package/build/src/client/types.js +3 -0
  56. package/build/src/client/types.js.map +1 -0
  57. package/build/src/codegen/bin/cli.d.ts +45 -0
  58. package/build/src/codegen/bin/cli.js +246 -0
  59. package/build/src/codegen/bin/cli.js.map +1 -0
  60. package/build/src/codegen/bin/cli.test.d.ts +1 -0
  61. package/build/src/codegen/bin/cli.test.js +220 -0
  62. package/build/src/codegen/bin/cli.test.js.map +1 -0
  63. package/build/src/codegen/constants.d.ts +1 -0
  64. package/build/src/codegen/constants.js +2 -0
  65. package/build/src/codegen/constants.js.map +1 -0
  66. package/build/src/codegen/e2e.test.d.ts +1 -0
  67. package/build/src/codegen/e2e.test.js +464 -0
  68. package/build/src/codegen/e2e.test.js.map +1 -0
  69. package/build/src/codegen/emit-client-runtime.d.ts +9 -0
  70. package/build/src/codegen/emit-client-runtime.js +99 -0
  71. package/build/src/codegen/emit-client-runtime.js.map +1 -0
  72. package/build/src/codegen/emit-client-runtime.test.d.ts +1 -0
  73. package/build/src/codegen/emit-client-runtime.test.js +78 -0
  74. package/build/src/codegen/emit-client-runtime.test.js.map +1 -0
  75. package/build/src/codegen/emit-client-types.d.ts +8 -0
  76. package/build/src/codegen/emit-client-types.js +25 -0
  77. package/build/src/codegen/emit-client-types.js.map +1 -0
  78. package/build/src/codegen/emit-client-types.test.d.ts +1 -0
  79. package/build/src/codegen/emit-client-types.test.js +33 -0
  80. package/build/src/codegen/emit-client-types.test.js.map +1 -0
  81. package/build/src/codegen/emit-errors.d.ts +19 -0
  82. package/build/src/codegen/emit-errors.js +59 -0
  83. package/build/src/codegen/emit-errors.js.map +1 -0
  84. package/build/src/codegen/emit-errors.test.d.ts +1 -0
  85. package/build/src/codegen/emit-errors.test.js +175 -0
  86. package/build/src/codegen/emit-errors.test.js.map +1 -0
  87. package/build/src/codegen/emit-index.d.ts +12 -0
  88. package/build/src/codegen/emit-index.js +41 -0
  89. package/build/src/codegen/emit-index.js.map +1 -0
  90. package/build/src/codegen/emit-index.test.d.ts +1 -0
  91. package/build/src/codegen/emit-index.test.js +106 -0
  92. package/build/src/codegen/emit-index.test.js.map +1 -0
  93. package/build/src/codegen/emit-scope.d.ts +15 -0
  94. package/build/src/codegen/emit-scope.js +299 -0
  95. package/build/src/codegen/emit-scope.js.map +1 -0
  96. package/build/src/codegen/emit-scope.test.d.ts +1 -0
  97. package/build/src/codegen/emit-scope.test.js +559 -0
  98. package/build/src/codegen/emit-scope.test.js.map +1 -0
  99. package/build/src/codegen/emit-types.d.ts +43 -0
  100. package/build/src/codegen/emit-types.js +111 -0
  101. package/build/src/codegen/emit-types.js.map +1 -0
  102. package/build/src/codegen/emit-types.test.d.ts +1 -0
  103. package/build/src/codegen/emit-types.test.js +184 -0
  104. package/build/src/codegen/emit-types.test.js.map +1 -0
  105. package/build/src/codegen/group-routes.d.ts +23 -0
  106. package/build/src/codegen/group-routes.js +46 -0
  107. package/build/src/codegen/group-routes.js.map +1 -0
  108. package/build/src/codegen/group-routes.test.d.ts +1 -0
  109. package/build/src/codegen/group-routes.test.js +131 -0
  110. package/build/src/codegen/group-routes.test.js.map +1 -0
  111. package/build/src/codegen/index.d.ts +15 -0
  112. package/build/src/codegen/index.js +16 -0
  113. package/build/src/codegen/index.js.map +1 -0
  114. package/build/src/codegen/naming.d.ts +7 -0
  115. package/build/src/codegen/naming.js +21 -0
  116. package/build/src/codegen/naming.js.map +1 -0
  117. package/build/src/codegen/naming.test.d.ts +1 -0
  118. package/build/src/codegen/naming.test.js +40 -0
  119. package/build/src/codegen/naming.test.js.map +1 -0
  120. package/build/src/codegen/pipeline.d.ts +17 -0
  121. package/build/src/codegen/pipeline.js +78 -0
  122. package/build/src/codegen/pipeline.js.map +1 -0
  123. package/build/src/codegen/pipeline.test.d.ts +1 -0
  124. package/build/src/codegen/pipeline.test.js +269 -0
  125. package/build/src/codegen/pipeline.test.js.map +1 -0
  126. package/build/src/codegen/resolve-envelope.d.ts +7 -0
  127. package/build/src/codegen/resolve-envelope.js +46 -0
  128. package/build/src/codegen/resolve-envelope.js.map +1 -0
  129. package/build/src/codegen/resolve-envelope.test.d.ts +1 -0
  130. package/build/src/codegen/resolve-envelope.test.js +69 -0
  131. package/build/src/codegen/resolve-envelope.test.js.map +1 -0
  132. package/build/src/errors.d.ts +33 -0
  133. package/build/src/errors.js +91 -0
  134. package/build/src/errors.js.map +1 -0
  135. package/build/src/errors.test.d.ts +1 -0
  136. package/build/src/errors.test.js +122 -0
  137. package/build/src/errors.test.js.map +1 -0
  138. package/build/src/exports.d.ts +7 -0
  139. package/build/src/exports.js +8 -0
  140. package/build/src/exports.js.map +1 -0
  141. package/build/src/implementations/http/doc-registry.d.ts +12 -0
  142. package/build/src/implementations/http/doc-registry.js +114 -0
  143. package/build/src/implementations/http/doc-registry.js.map +1 -0
  144. package/build/src/implementations/http/doc-registry.test.d.ts +1 -0
  145. package/build/src/implementations/http/doc-registry.test.js +347 -0
  146. package/build/src/implementations/http/doc-registry.test.js.map +1 -0
  147. package/build/src/implementations/http/express-rpc/index.d.ts +94 -0
  148. package/build/src/implementations/http/express-rpc/index.js +185 -0
  149. package/build/src/implementations/http/express-rpc/index.js.map +1 -0
  150. package/build/src/implementations/http/express-rpc/index.test.d.ts +1 -0
  151. package/build/src/implementations/http/express-rpc/index.test.js +684 -0
  152. package/build/src/implementations/http/express-rpc/index.test.js.map +1 -0
  153. package/build/src/implementations/http/express-rpc/types.d.ts +11 -0
  154. package/build/src/implementations/http/express-rpc/types.js +2 -0
  155. package/build/src/implementations/http/express-rpc/types.js.map +1 -0
  156. package/build/src/implementations/http/hono-api/index.d.ts +102 -0
  157. package/build/src/implementations/http/hono-api/index.js +341 -0
  158. package/build/src/implementations/http/hono-api/index.js.map +1 -0
  159. package/build/src/implementations/http/hono-api/index.test.d.ts +1 -0
  160. package/build/src/implementations/http/hono-api/index.test.js +992 -0
  161. package/build/src/implementations/http/hono-api/index.test.js.map +1 -0
  162. package/build/src/implementations/http/hono-api/types.d.ts +13 -0
  163. package/build/src/implementations/http/hono-api/types.js +2 -0
  164. package/build/src/implementations/http/hono-api/types.js.map +1 -0
  165. package/build/src/implementations/http/hono-rpc/index.d.ts +92 -0
  166. package/build/src/implementations/http/hono-rpc/index.js +161 -0
  167. package/build/src/implementations/http/hono-rpc/index.js.map +1 -0
  168. package/build/src/implementations/http/hono-rpc/index.test.d.ts +1 -0
  169. package/build/src/implementations/http/hono-rpc/index.test.js +803 -0
  170. package/build/src/implementations/http/hono-rpc/index.test.js.map +1 -0
  171. package/build/src/implementations/http/hono-rpc/types.d.ts +11 -0
  172. package/build/src/implementations/http/hono-rpc/types.js +2 -0
  173. package/build/src/implementations/http/hono-rpc/types.js.map +1 -0
  174. package/build/src/implementations/http/hono-stream/index.d.ts +120 -0
  175. package/build/src/implementations/http/hono-stream/index.js +309 -0
  176. package/build/src/implementations/http/hono-stream/index.js.map +1 -0
  177. package/build/src/implementations/http/hono-stream/index.test.d.ts +1 -0
  178. package/build/src/implementations/http/hono-stream/index.test.js +1356 -0
  179. package/build/src/implementations/http/hono-stream/index.test.js.map +1 -0
  180. package/build/src/implementations/http/hono-stream/types.d.ts +15 -0
  181. package/build/src/implementations/http/hono-stream/types.js +2 -0
  182. package/build/src/implementations/http/hono-stream/types.js.map +1 -0
  183. package/build/src/implementations/types.d.ts +142 -0
  184. package/build/src/implementations/types.js +2 -0
  185. package/build/src/implementations/types.js.map +1 -0
  186. package/build/src/index.d.ts +165 -0
  187. package/build/src/index.js +253 -0
  188. package/build/src/index.js.map +1 -0
  189. package/build/src/index.test.d.ts +1 -0
  190. package/build/src/index.test.js +890 -0
  191. package/build/src/index.test.js.map +1 -0
  192. package/build/src/schema/compute-schema.d.ts +35 -0
  193. package/build/src/schema/compute-schema.js +41 -0
  194. package/build/src/schema/compute-schema.js.map +1 -0
  195. package/build/src/schema/compute-schema.test.d.ts +1 -0
  196. package/build/src/schema/compute-schema.test.js +107 -0
  197. package/build/src/schema/compute-schema.test.js.map +1 -0
  198. package/build/src/schema/extract-json-schema.d.ts +2 -0
  199. package/build/src/schema/extract-json-schema.js +12 -0
  200. package/build/src/schema/extract-json-schema.js.map +1 -0
  201. package/build/src/schema/extract-json-schema.test.d.ts +1 -0
  202. package/build/src/schema/extract-json-schema.test.js +23 -0
  203. package/build/src/schema/extract-json-schema.test.js.map +1 -0
  204. package/build/src/schema/parser.d.ts +28 -0
  205. package/build/src/schema/parser.js +170 -0
  206. package/build/src/schema/parser.js.map +1 -0
  207. package/build/src/schema/parser.test.d.ts +1 -0
  208. package/build/src/schema/parser.test.js +120 -0
  209. package/build/src/schema/parser.test.js.map +1 -0
  210. package/build/src/schema/resolve-schema-lib.d.ts +12 -0
  211. package/build/src/schema/resolve-schema-lib.js +11 -0
  212. package/build/src/schema/resolve-schema-lib.js.map +1 -0
  213. package/build/src/schema/resolve-schema-lib.test.d.ts +1 -0
  214. package/build/src/schema/resolve-schema-lib.test.js +17 -0
  215. package/build/src/schema/resolve-schema-lib.test.js.map +1 -0
  216. package/build/src/schema/types.d.ts +8 -0
  217. package/build/src/schema/types.js +2 -0
  218. package/build/src/schema/types.js.map +1 -0
  219. package/build/src/stack-utils.d.ts +25 -0
  220. package/build/src/stack-utils.js +95 -0
  221. package/build/src/stack-utils.js.map +1 -0
  222. package/build/src/stack-utils.test.d.ts +1 -0
  223. package/build/src/stack-utils.test.js +80 -0
  224. package/build/src/stack-utils.test.js.map +1 -0
  225. package/docs/ai-agent-setup.md +7 -6
  226. package/docs/core.md +5 -9
  227. package/docs/streaming.md +9 -9
  228. package/package.json +2 -13
  229. package/src/client/call.test.ts +162 -0
  230. package/src/client/errors.test.ts +43 -0
  231. package/src/client/fetch-adapter.test.ts +340 -0
  232. package/src/client/hooks.test.ts +191 -0
  233. package/src/client/index.test.ts +290 -0
  234. package/src/client/request-builder.test.ts +184 -0
  235. package/src/client/stream.test.ts +331 -0
  236. package/src/codegen/bin/cli.test.ts +260 -0
  237. package/src/codegen/bin/cli.ts +282 -0
  238. package/src/codegen/constants.ts +1 -0
  239. package/src/codegen/e2e.test.ts +565 -0
  240. package/src/codegen/emit-client-runtime.test.ts +93 -0
  241. package/src/codegen/emit-client-runtime.ts +114 -0
  242. package/src/codegen/emit-client-types.test.ts +39 -0
  243. package/src/codegen/emit-client-types.ts +27 -0
  244. package/src/codegen/emit-errors.test.ts +202 -0
  245. package/src/codegen/emit-errors.ts +80 -0
  246. package/src/codegen/emit-index.test.ts +127 -0
  247. package/src/codegen/emit-index.ts +58 -0
  248. package/src/codegen/emit-scope.test.ts +624 -0
  249. package/src/codegen/emit-scope.ts +389 -0
  250. package/src/codegen/emit-types.test.ts +205 -0
  251. package/src/codegen/emit-types.ts +158 -0
  252. package/src/codegen/group-routes.test.ts +159 -0
  253. package/src/codegen/group-routes.ts +61 -0
  254. package/src/codegen/index.ts +30 -0
  255. package/src/codegen/naming.test.ts +50 -0
  256. package/src/codegen/naming.ts +25 -0
  257. package/src/codegen/pipeline.test.ts +316 -0
  258. package/src/codegen/pipeline.ts +108 -0
  259. package/src/codegen/resolve-envelope.test.ts +76 -0
  260. package/src/codegen/resolve-envelope.ts +61 -0
  261. package/src/errors.test.ts +163 -0
  262. package/src/errors.ts +107 -0
  263. package/src/exports.ts +7 -0
  264. package/src/implementations/http/doc-registry.test.ts +415 -0
  265. package/src/implementations/http/doc-registry.ts +143 -0
  266. package/src/implementations/http/express-rpc/README.md +6 -6
  267. package/src/implementations/http/express-rpc/index.test.ts +957 -0
  268. package/src/implementations/http/express-rpc/index.ts +266 -0
  269. package/src/implementations/http/express-rpc/types.ts +16 -0
  270. package/src/implementations/http/hono-api/index.test.ts +1341 -0
  271. package/src/implementations/http/hono-api/index.ts +463 -0
  272. package/src/implementations/http/hono-api/types.ts +16 -0
  273. package/src/implementations/http/hono-rpc/README.md +6 -6
  274. package/src/implementations/http/hono-rpc/index.test.ts +1075 -0
  275. package/src/implementations/http/hono-rpc/index.ts +238 -0
  276. package/src/implementations/http/hono-rpc/types.ts +16 -0
  277. package/src/implementations/http/hono-stream/README.md +12 -12
  278. package/src/implementations/http/hono-stream/index.test.ts +1768 -0
  279. package/src/implementations/http/hono-stream/index.ts +456 -0
  280. package/src/implementations/http/hono-stream/types.ts +20 -0
  281. package/src/implementations/types.ts +174 -0
  282. package/src/index.test.ts +1185 -0
  283. package/src/index.ts +522 -0
  284. package/src/schema/compute-schema.test.ts +128 -0
  285. package/src/schema/compute-schema.ts +88 -0
  286. package/src/schema/extract-json-schema.test.ts +25 -0
  287. package/src/schema/extract-json-schema.ts +15 -0
  288. package/src/schema/parser.test.ts +182 -0
  289. package/src/schema/parser.ts +215 -0
  290. package/src/schema/resolve-schema-lib.test.ts +19 -0
  291. package/src/schema/resolve-schema-lib.ts +29 -0
  292. package/src/schema/types.ts +20 -0
  293. package/src/stack-utils.test.ts +94 -0
  294. package/src/stack-utils.ts +129 -0
  295. package/agent_config/claude-code/skills/review/SKILL.md +0 -53
  296. package/docs/superpowers/plans/2026-03-30-client-codegen.md +0 -2833
  297. package/docs/superpowers/specs/2026-03-30-client-codegen-design.md +0 -632
  298. /package/agent_config/claude-code/skills/{guide → ts-procedures}/patterns.md +0 -0
  299. /package/agent_config/claude-code/skills/{review → ts-procedures-review}/checklist.md +0 -0
  300. /package/agent_config/claude-code/skills/{scaffold → ts-procedures-scaffold}/templates/express-rpc.md +0 -0
  301. /package/agent_config/claude-code/skills/{scaffold → ts-procedures-scaffold}/templates/hono-api.md +0 -0
  302. /package/agent_config/claude-code/skills/{scaffold → ts-procedures-scaffold}/templates/hono-rpc.md +0 -0
  303. /package/agent_config/claude-code/skills/{scaffold → ts-procedures-scaffold}/templates/hono-stream.md +0 -0
  304. /package/agent_config/claude-code/skills/{scaffold → ts-procedures-scaffold}/templates/procedure.md +0 -0
  305. /package/agent_config/claude-code/skills/{scaffold → ts-procedures-scaffold}/templates/stream-procedure.md +0 -0
@@ -0,0 +1,684 @@
1
+ import { describe, expect, test, vi, beforeEach } from 'vitest';
2
+ import request from 'supertest';
3
+ import express from 'express';
4
+ import { v } from 'suretype';
5
+ import { Procedures } from '../../../index.js';
6
+ import { ExpressRPCAppBuilder } from './index.js';
7
+ /**
8
+ * ExpressRPCAppBuilder Test Suite
9
+ *
10
+ * Tests the RPC-style Express integration for ts-procedures.
11
+ * This builder creates POST routes at `/{name}/{version}` paths (with optional pathPrefix).
12
+ */
13
+ describe('ExpressRPCAppBuilder', () => {
14
+ // --------------------------------------------------------------------------
15
+ // Constructor Tests
16
+ // --------------------------------------------------------------------------
17
+ describe('constructor', () => {
18
+ test('creates default Express app with json middleware', async () => {
19
+ const builder = new ExpressRPCAppBuilder();
20
+ const RPC = Procedures();
21
+ RPC.Create('Echo', { scope: 'echo', version: 1, schema: { params: v.object({ message: v.string() }) } }, async (ctx, params) => params);
22
+ builder.register(RPC, () => ({ userId: '123' }));
23
+ const app = builder.build();
24
+ // JSON body should be parsed automatically
25
+ const res = await request(app).post('/echo/echo/1').send({ message: 'hello' });
26
+ expect(res.status).toBe(200);
27
+ expect(res.body).toEqual({ message: 'hello' });
28
+ });
29
+ test('uses provided Express app without adding middleware', async () => {
30
+ const customApp = express();
31
+ // Intentionally NOT adding json middleware
32
+ const builder = new ExpressRPCAppBuilder({ app: customApp });
33
+ const RPC = Procedures();
34
+ RPC.Create('Echo', { scope: 'echo', version: 1, schema: { params: v.object({ message: v.string() }) } }, async (ctx, params) => ({ received: params }));
35
+ builder.register(RPC, () => ({ userId: '123' }));
36
+ const app = builder.build();
37
+ // Without json middleware, body won't be parsed (req.body is undefined)
38
+ const res = await request(app)
39
+ .post('/echo/echo/1')
40
+ .set('Content-Type', 'application/json')
41
+ .send(JSON.stringify({ message: 'hello' }));
42
+ // Request body is undefined since json middleware wasn't added
43
+ // Handler receives undefined params
44
+ expect(res.body.received).toBeUndefined();
45
+ });
46
+ test('handles empty config', () => {
47
+ const builder = new ExpressRPCAppBuilder({});
48
+ expect(builder.app).toBeDefined();
49
+ expect(builder.docs).toEqual([]);
50
+ });
51
+ test('handles undefined config', () => {
52
+ const builder = new ExpressRPCAppBuilder(undefined);
53
+ expect(builder.app).toBeDefined();
54
+ expect(builder.docs).toEqual([]);
55
+ });
56
+ });
57
+ // --------------------------------------------------------------------------
58
+ // pathPrefix Option Tests
59
+ // --------------------------------------------------------------------------
60
+ describe('pathPrefix option', () => {
61
+ test('uses custom pathPrefix for all routes', async () => {
62
+ const builder = new ExpressRPCAppBuilder({ pathPrefix: '/api/v1' });
63
+ const RPC = Procedures();
64
+ RPC.Create('Test', { scope: 'test', version: 1 }, async () => ({ ok: true }));
65
+ builder.register(RPC, () => ({}));
66
+ const app = builder.build();
67
+ const res = await request(app).post('/api/v1/test/test/1').send({});
68
+ expect(res.status).toBe(200);
69
+ expect(res.body).toEqual({ ok: true });
70
+ });
71
+ test('pathPrefix without leading slash gets normalized', async () => {
72
+ const builder = new ExpressRPCAppBuilder({ pathPrefix: 'custom' });
73
+ const RPC = Procedures();
74
+ RPC.Create('Test', { scope: 'test', version: 1 }, async () => ({ ok: true }));
75
+ builder.register(RPC, () => ({}));
76
+ const app = builder.build();
77
+ const res = await request(app).post('/custom/test/test/1').send({});
78
+ expect(res.status).toBe(200);
79
+ });
80
+ test('no prefix when pathPrefix not specified', async () => {
81
+ const builder = new ExpressRPCAppBuilder();
82
+ const RPC = Procedures();
83
+ RPC.Create('Test', { scope: 'test', version: 1 }, async () => ({ ok: true }));
84
+ builder.register(RPC, () => ({}));
85
+ const app = builder.build();
86
+ const res = await request(app).post('/test/test/1').send({});
87
+ expect(res.status).toBe(200);
88
+ });
89
+ test('pathPrefix appears in generated docs', () => {
90
+ const builder = new ExpressRPCAppBuilder({ pathPrefix: '/api' });
91
+ const RPC = Procedures();
92
+ RPC.Create('Test', { scope: 'test', version: 1 }, async () => ({}));
93
+ builder.register(RPC, () => ({}));
94
+ builder.build();
95
+ expect(builder.docs[0].path).toBe('/api/test/test/1');
96
+ });
97
+ test('pathPrefix /rpc restores original behavior', async () => {
98
+ const builder = new ExpressRPCAppBuilder({ pathPrefix: '/rpc' });
99
+ const RPC = Procedures();
100
+ RPC.Create('Users', { scope: 'users', version: 1 }, async () => ({ users: [] }));
101
+ builder.register(RPC, () => ({}));
102
+ const app = builder.build();
103
+ const res = await request(app).post('/rpc/users/users/1').send({});
104
+ expect(res.status).toBe(200);
105
+ expect(builder.docs[0].path).toBe('/rpc/users/users/1');
106
+ });
107
+ });
108
+ // --------------------------------------------------------------------------
109
+ // Lifecycle Hooks Tests
110
+ // --------------------------------------------------------------------------
111
+ describe('lifecycle hooks', () => {
112
+ test('onRequestStart is called with request object', async () => {
113
+ const onRequestStart = vi.fn();
114
+ const builder = new ExpressRPCAppBuilder({ onRequestStart });
115
+ const RPC = Procedures();
116
+ RPC.Create('Test', { scope: 'test', version: 1 }, async () => ({ ok: true }));
117
+ builder.register(RPC, () => ({}));
118
+ const app = builder.build();
119
+ await request(app).post('/test/test/1').send({});
120
+ expect(onRequestStart).toHaveBeenCalledTimes(1);
121
+ expect(onRequestStart.mock.calls[0][0]).toHaveProperty('method', 'POST');
122
+ expect(onRequestStart.mock.calls[0][0]).toHaveProperty('path', '/test/test/1');
123
+ });
124
+ test('onRequestEnd is called after response finishes', async () => {
125
+ const onRequestEnd = vi.fn();
126
+ const builder = new ExpressRPCAppBuilder({ onRequestEnd });
127
+ const RPC = Procedures();
128
+ RPC.Create('Test', { scope: 'test', version: 1 }, async () => ({ ok: true }));
129
+ builder.register(RPC, () => ({}));
130
+ const app = builder.build();
131
+ await request(app).post('/test/test/1').send({});
132
+ expect(onRequestEnd).toHaveBeenCalledTimes(1);
133
+ expect(onRequestEnd.mock.calls[0][0]).toHaveProperty('method', 'POST');
134
+ expect(onRequestEnd.mock.calls[0][1]).toHaveProperty('statusCode', 200);
135
+ });
136
+ test('onSuccess is called on successful procedure execution', async () => {
137
+ const onSuccess = vi.fn();
138
+ const builder = new ExpressRPCAppBuilder({ onSuccess });
139
+ const RPC = Procedures();
140
+ RPC.Create('Test', { scope: 'test', version: 1 }, async () => ({ ok: true }));
141
+ builder.register(RPC, () => ({}));
142
+ const app = builder.build();
143
+ await request(app).post('/test/test/1').send({});
144
+ expect(onSuccess).toHaveBeenCalledTimes(1);
145
+ expect(onSuccess.mock.calls[0][0]).toHaveProperty('name', 'Test');
146
+ });
147
+ test('onSuccess is NOT called when procedure throws', async () => {
148
+ const onSuccess = vi.fn();
149
+ const builder = new ExpressRPCAppBuilder({ onSuccess });
150
+ const RPC = Procedures();
151
+ RPC.Create('Test', { scope: 'test', version: 1 }, async () => {
152
+ throw new Error('Handler error');
153
+ });
154
+ builder.register(RPC, () => ({}));
155
+ const app = builder.build();
156
+ await request(app).post('/test/test/1').send({});
157
+ expect(onSuccess).not.toHaveBeenCalled();
158
+ });
159
+ test('hooks execute in correct order: start → handler → success → end', async () => {
160
+ const order = [];
161
+ const builder = new ExpressRPCAppBuilder({
162
+ onRequestStart: () => order.push('start'),
163
+ onRequestEnd: () => order.push('end'),
164
+ onSuccess: () => order.push('success'),
165
+ });
166
+ const RPC = Procedures();
167
+ RPC.Create('Test', { scope: 'test', version: 1 }, async () => {
168
+ order.push('handler');
169
+ return { ok: true };
170
+ });
171
+ builder.register(RPC, () => ({}));
172
+ const app = builder.build();
173
+ await request(app).post('/test/test/1').send({});
174
+ expect(order).toEqual(['start', 'handler', 'success', 'end']);
175
+ });
176
+ });
177
+ // --------------------------------------------------------------------------
178
+ // Error Handling Tests
179
+ // --------------------------------------------------------------------------
180
+ describe('error handling', () => {
181
+ test('custom error handler receives procedure, req, res, and error', async () => {
182
+ const errorHandler = vi.fn((procedure, req, res, error) => {
183
+ res.status(400).json({ customError: error.message });
184
+ });
185
+ const builder = new ExpressRPCAppBuilder({ onError: errorHandler });
186
+ const RPC = Procedures();
187
+ RPC.Create('Test', { scope: 'test', version: 1 }, async () => {
188
+ throw new Error('Test error');
189
+ });
190
+ builder.register(RPC, () => ({}));
191
+ const app = builder.build();
192
+ const res = await request(app).post('/test/test/1').send({});
193
+ expect(errorHandler).toHaveBeenCalledTimes(1);
194
+ expect(errorHandler.mock.calls[0][0]).toHaveProperty('name', 'Test');
195
+ expect(errorHandler.mock.calls[0][3]).toBeInstanceOf(Error);
196
+ expect(res.status).toBe(400);
197
+ // Error is wrapped by Procedures with "Error in handler for {name}" prefix
198
+ expect(res.body.customError).toContain('Test error');
199
+ });
200
+ test('default error handling returns error message in response', async () => {
201
+ const builder = new ExpressRPCAppBuilder();
202
+ const RPC = Procedures();
203
+ RPC.Create('Test', { scope: 'test', version: 1 }, async () => {
204
+ throw new Error('Something went wrong');
205
+ });
206
+ builder.register(RPC, () => ({}));
207
+ const app = builder.build();
208
+ const res = await request(app).post('/test/test/1').send({});
209
+ // Default error handler returns error message in JSON body
210
+ expect(res.body).toHaveProperty('error');
211
+ expect(res.body.error).toContain('Something went wrong');
212
+ });
213
+ test('catches unhandled exceptions in handler', async () => {
214
+ const builder = new ExpressRPCAppBuilder();
215
+ const RPC = Procedures();
216
+ RPC.Create('Test', { scope: 'test', version: 1 }, async () => {
217
+ // Simulate unhandled exception
218
+ const obj = null;
219
+ return obj.property; // This will throw
220
+ });
221
+ builder.register(RPC, () => ({}));
222
+ const app = builder.build();
223
+ const res = await request(app).post('/test/test/1').send({});
224
+ // Unhandled exceptions are caught and returned as error response
225
+ expect(res.body).toHaveProperty('error');
226
+ });
227
+ });
228
+ // --------------------------------------------------------------------------
229
+ // register() Method Tests
230
+ // --------------------------------------------------------------------------
231
+ describe('register() method', () => {
232
+ test('returns this for method chaining', () => {
233
+ const builder = new ExpressRPCAppBuilder();
234
+ const RPC1 = Procedures();
235
+ const RPC2 = Procedures();
236
+ const result = builder.register(RPC1, () => ({}));
237
+ expect(result).toBe(builder);
238
+ // Chain multiple registrations
239
+ const chainResult = builder.register(RPC1, () => ({})).register(RPC2, () => ({}));
240
+ expect(chainResult).toBe(builder);
241
+ });
242
+ test('supports registering multiple factories', async () => {
243
+ const builder = new ExpressRPCAppBuilder();
244
+ const PublicRPC = Procedures();
245
+ const PrivateRPC = Procedures();
246
+ PublicRPC.Create('PublicMethod', { scope: 'public', version: 1 }, async (ctx) => ({
247
+ isPublic: ctx.public,
248
+ }));
249
+ PrivateRPC.Create('PrivateMethod', { scope: 'private', version: 1 }, async (ctx) => ({
250
+ isPrivate: ctx.private,
251
+ }));
252
+ builder
253
+ .register(PublicRPC, () => ({ public: true }))
254
+ .register(PrivateRPC, () => ({ private: true }));
255
+ const app = builder.build();
256
+ const publicRes = await request(app).post('/public/public-method/1').send({});
257
+ const privateRes = await request(app).post('/private/private-method/1').send({});
258
+ expect(publicRes.body).toEqual({ isPublic: true });
259
+ expect(privateRes.body).toEqual({ isPrivate: true });
260
+ });
261
+ test('context can be a static object', async () => {
262
+ const factoryContext = { requestId: 'req-123' };
263
+ const builder = new ExpressRPCAppBuilder();
264
+ const RPC = Procedures();
265
+ RPC.Create('GetRequestId', { scope: 'get-request-id', version: 1 }, async (ctx) => ({
266
+ id: ctx.requestId,
267
+ }));
268
+ builder.register(RPC, factoryContext);
269
+ const app = builder.build();
270
+ const res = await request(app).post('/get-request-id/get-request-id/1').send({});
271
+ expect(res.body).toEqual({ id: 'req-123' });
272
+ });
273
+ test('factoryContext can be async function', async () => {
274
+ const factoryContext = vi.fn(async () => {
275
+ return { requestId: 'req-456' };
276
+ });
277
+ const builder = new ExpressRPCAppBuilder();
278
+ const RPC = Procedures();
279
+ RPC.Create('GetRequestId', { scope: 'get-request-id', version: 1 }, async (ctx) => ({
280
+ id: ctx.requestId,
281
+ }));
282
+ builder.register(RPC, factoryContext);
283
+ const app = builder.build();
284
+ await request(app).post('/get-request-id/get-request-id/1').send({});
285
+ expect(factoryContext).toHaveBeenCalledTimes(1);
286
+ });
287
+ test('factoryContext function receives Express request object', async () => {
288
+ const factoryContext = vi.fn((req) => ({
289
+ authHeader: req.headers.authorization,
290
+ }));
291
+ const builder = new ExpressRPCAppBuilder();
292
+ const RPC = Procedures();
293
+ RPC.Create('GetAuth', { scope: 'get-auth', version: 1 }, async (ctx) => ({
294
+ auth: ctx.authHeader,
295
+ }));
296
+ builder.register(RPC, factoryContext);
297
+ const app = builder.build();
298
+ await request(app)
299
+ .post('/get-auth/get-auth/1')
300
+ .set('Authorization', 'Bearer token123')
301
+ .send({});
302
+ expect(factoryContext).toHaveBeenCalledTimes(1);
303
+ expect(factoryContext.mock.calls[0][0]).toHaveProperty('headers');
304
+ expect(factoryContext.mock.calls[0][0].headers).toHaveProperty('authorization', 'Bearer token123');
305
+ });
306
+ });
307
+ // --------------------------------------------------------------------------
308
+ // build() Method Tests
309
+ // --------------------------------------------------------------------------
310
+ describe('build() method', () => {
311
+ test('creates POST routes for all procedures', async () => {
312
+ const builder = new ExpressRPCAppBuilder();
313
+ const RPC = Procedures();
314
+ RPC.Create('MethodOne', { scope: 'method-one', version: 1 }, async () => ({ m: 1 }));
315
+ RPC.Create('MethodTwo', { scope: 'method-two', version: 2 }, async () => ({ m: 2 }));
316
+ builder.register(RPC, () => ({}));
317
+ const app = builder.build();
318
+ const res1 = await request(app).post('/method-one/method-one/1').send({});
319
+ const res2 = await request(app).post('/method-two/method-two/2').send({});
320
+ expect(res1.status).toBe(200);
321
+ expect(res2.status).toBe(200);
322
+ expect(res1.body).toEqual({ m: 1 });
323
+ expect(res2.body).toEqual({ m: 2 });
324
+ });
325
+ test('returns the Express application', () => {
326
+ const builder = new ExpressRPCAppBuilder();
327
+ const app = builder.build();
328
+ expect(app).toBe(builder.app);
329
+ expect(typeof app.listen).toBe('function');
330
+ });
331
+ test('populates docs array after build', () => {
332
+ const builder = new ExpressRPCAppBuilder();
333
+ const RPC = Procedures();
334
+ RPC.Create('MethodOne', { scope: 'method-one', version: 1 }, async () => ({}));
335
+ RPC.Create('MethodTwo', { scope: ['nested', 'method'], version: 2 }, async () => ({}));
336
+ expect(builder.docs).toHaveLength(0);
337
+ builder.register(RPC, () => ({}));
338
+ builder.build();
339
+ expect(builder.docs).toHaveLength(2);
340
+ expect(builder.docs[0].path).toBe('/method-one/method-one/1');
341
+ expect(builder.docs[1].path).toBe('/nested/method/method-two/2');
342
+ });
343
+ test('passes request body to handler as params', async () => {
344
+ const builder = new ExpressRPCAppBuilder();
345
+ const RPC = Procedures();
346
+ RPC.Create('Echo', { scope: 'echo', version: 1, schema: { params: v.object({ data: v.string() }) } }, async (ctx, params) => ({ received: params.data }));
347
+ builder.register(RPC, () => ({}));
348
+ const app = builder.build();
349
+ const res = await request(app).post('/echo/echo/1').send({ data: 'test-data' });
350
+ expect(res.body).toEqual({ received: 'test-data' });
351
+ });
352
+ test('GET requests return 404 (RPC uses POST only)', async () => {
353
+ const builder = new ExpressRPCAppBuilder();
354
+ const RPC = Procedures();
355
+ RPC.Create('Test', { scope: 'test', version: 1 }, async () => ({ ok: true }));
356
+ builder.register(RPC, () => ({}));
357
+ const app = builder.build();
358
+ const res = await request(app).get('/test/test/1');
359
+ expect(res.status).toBe(404);
360
+ });
361
+ });
362
+ // --------------------------------------------------------------------------
363
+ // Path Generation Tests (makeRPCHttpRoutePath)
364
+ // --------------------------------------------------------------------------
365
+ describe('makeRPCHttpRoutePath', () => {
366
+ let builder;
367
+ beforeEach(() => {
368
+ builder = new ExpressRPCAppBuilder();
369
+ });
370
+ test("simple scope with procedure name: 'users' + 'GetUser' → /users/get-user/1", () => {
371
+ const path = builder.makeRPCHttpRoutePath('GetUser', { scope: 'users', version: 1 });
372
+ expect(path).toBe('/users/get-user/1');
373
+ });
374
+ test("array scope with procedure name: ['users', 'profile'] + 'GetById' → /users/profile/get-by-id/1", () => {
375
+ const path = builder.makeRPCHttpRoutePath('GetById', {
376
+ scope: ['users', 'profile'],
377
+ version: 1,
378
+ });
379
+ expect(path).toBe('/users/profile/get-by-id/1');
380
+ });
381
+ test("camelCase procedure name: 'users' + 'getProfile' → /users/get-profile/1", () => {
382
+ const path = builder.makeRPCHttpRoutePath('getProfile', { scope: 'users', version: 1 });
383
+ expect(path).toBe('/users/get-profile/1');
384
+ });
385
+ test("PascalCase procedure name: 'users' + 'UpdateProfile' → /users/update-profile/1", () => {
386
+ const path = builder.makeRPCHttpRoutePath('UpdateProfile', { scope: 'users', version: 1 });
387
+ expect(path).toBe('/users/update-profile/1');
388
+ });
389
+ test('version number included in path', () => {
390
+ const pathV1 = builder.makeRPCHttpRoutePath('Test', { scope: 'test', version: 1 });
391
+ const pathV2 = builder.makeRPCHttpRoutePath('Test', { scope: 'test', version: 2 });
392
+ const pathV99 = builder.makeRPCHttpRoutePath('Test', { scope: 'test', version: 99 });
393
+ expect(pathV1).toBe('/test/test/1');
394
+ expect(pathV2).toBe('/test/test/2');
395
+ expect(pathV99).toBe('/test/test/99');
396
+ });
397
+ test('handles mixed case in array segments', () => {
398
+ const path = builder.makeRPCHttpRoutePath('ListUsers', {
399
+ scope: ['UserModule', 'getActiveUsers'],
400
+ version: 1,
401
+ });
402
+ expect(path).toBe('/user-module/get-active-users/list-users/1');
403
+ });
404
+ });
405
+ // --------------------------------------------------------------------------
406
+ // Route Documentation Tests (buildRpcHttpRouteDoc)
407
+ // --------------------------------------------------------------------------
408
+ describe('buildRpcHttpRouteDoc', () => {
409
+ let builder;
410
+ beforeEach(() => {
411
+ builder = new ExpressRPCAppBuilder();
412
+ });
413
+ test('generates complete route documentation', () => {
414
+ const paramsSchema = v.object({ id: v.string() });
415
+ const returnSchema = v.object({ name: v.string() });
416
+ const RPC = Procedures();
417
+ RPC.Create('GetUser', { scope: 'users', version: 1, schema: { params: paramsSchema, returnType: returnSchema } }, async () => ({ name: 'test' }));
418
+ builder.register(RPC, () => ({}));
419
+ builder.build();
420
+ const doc = builder.docs[0];
421
+ expect(doc.path).toBe('/users/get-user/1');
422
+ expect(doc.method).toBe('post');
423
+ expect(doc.jsonSchema.body).toBeDefined();
424
+ expect(doc.jsonSchema.response).toBeDefined();
425
+ });
426
+ test('omits body schema when no params defined', () => {
427
+ const RPC = Procedures();
428
+ RPC.Create('NoParams', { scope: 'no-params', version: 1 }, async () => ({ ok: true }));
429
+ builder.register(RPC, () => ({}));
430
+ builder.build();
431
+ const doc = builder.docs[0];
432
+ expect(doc.jsonSchema.body).toBeUndefined();
433
+ });
434
+ test('omits response schema when no returnType defined', () => {
435
+ const RPC = Procedures();
436
+ RPC.Create('NoReturn', { scope: 'no-return', version: 1, schema: { params: v.object({ x: v.number() }) } }, async () => ({}));
437
+ builder.register(RPC, () => ({}));
438
+ builder.build();
439
+ const doc = builder.docs[0];
440
+ expect(doc.jsonSchema.body).toBeDefined();
441
+ expect(doc.jsonSchema.response).toBeUndefined();
442
+ });
443
+ test("method is always 'post'", () => {
444
+ const RPC = Procedures();
445
+ RPC.Create('Test1', { scope: 't1', version: 1 }, async () => ({}));
446
+ RPC.Create('Test2', { scope: 't2', version: 2 }, async () => ({}));
447
+ builder.register(RPC, () => ({}));
448
+ builder.build();
449
+ builder.docs.forEach((doc) => {
450
+ expect(doc.method).toBe('post');
451
+ });
452
+ });
453
+ });
454
+ // --------------------------------------------------------------------------
455
+ // extendProcedureDoc Tests
456
+ // --------------------------------------------------------------------------
457
+ describe('extendProcedureDoc', () => {
458
+ test('adds custom properties to generated documentation', () => {
459
+ const builder = new ExpressRPCAppBuilder();
460
+ const RPC = Procedures();
461
+ RPC.Create('GetUser', { scope: 'users', version: 1 }, async () => ({ name: 'test' }));
462
+ builder.register(RPC, () => ({}), ({ base, procedure }) => ({
463
+ summary: `Get user endpoint`,
464
+ tags: ['users'],
465
+ operationId: procedure.name,
466
+ }));
467
+ builder.build();
468
+ const doc = builder.docs[0];
469
+ expect(doc).toHaveProperty('summary', 'Get user endpoint');
470
+ expect(doc).toHaveProperty('tags', ['users']);
471
+ expect(doc).toHaveProperty('operationId', 'GetUser');
472
+ });
473
+ test('receives correct base and procedure parameters', () => {
474
+ const extendFn = vi.fn(() => ({}));
475
+ const builder = new ExpressRPCAppBuilder();
476
+ const RPC = Procedures();
477
+ const paramsSchema = v.object({ id: v.string() });
478
+ RPC.Create('GetItem', { scope: 'items', version: 2, schema: { params: paramsSchema } }, async () => ({}));
479
+ builder.register(RPC, () => ({}), extendFn);
480
+ builder.build();
481
+ expect(extendFn).toHaveBeenCalledTimes(1);
482
+ const callArg = extendFn.mock.calls[0][0];
483
+ // Verify base properties
484
+ expect(callArg.base).toHaveProperty('name', 'GetItem');
485
+ expect(callArg.base).toHaveProperty('version', 2);
486
+ expect(callArg.base).toHaveProperty('scope', 'items');
487
+ expect(callArg.base).toHaveProperty('path', '/items/get-item/2');
488
+ expect(callArg.base).toHaveProperty('method', 'post');
489
+ expect(callArg.base.jsonSchema).toHaveProperty('body');
490
+ // Verify procedure properties
491
+ expect(callArg.procedure).toHaveProperty('name', 'GetItem');
492
+ expect(callArg.procedure).toHaveProperty('handler');
493
+ expect(callArg.procedure.config).toHaveProperty('scope', 'items');
494
+ expect(callArg.procedure.config).toHaveProperty('version', 2);
495
+ });
496
+ test('base properties take precedence over extended properties', () => {
497
+ const builder = new ExpressRPCAppBuilder();
498
+ const RPC = Procedures();
499
+ RPC.Create('Test', { scope: 'test', version: 1 }, async () => ({}));
500
+ builder.register(RPC, () => ({}), () => ({
501
+ name: 'OverriddenName',
502
+ path: '/overridden/path',
503
+ method: 'get',
504
+ customField: 'custom-value',
505
+ }));
506
+ builder.build();
507
+ const doc = builder.docs[0];
508
+ // Base properties should NOT be overridden
509
+ expect(doc.name).toBe('Test');
510
+ expect(doc.path).toBe('/test/test/1');
511
+ expect(doc.method).toBe('post');
512
+ // Custom field should be present
513
+ expect(doc).toHaveProperty('customField', 'custom-value');
514
+ });
515
+ test('different factories can have different extendProcedureDoc functions', () => {
516
+ const builder = new ExpressRPCAppBuilder();
517
+ const PublicRPC = Procedures();
518
+ const AdminRPC = Procedures();
519
+ PublicRPC.Create('GetPublic', { scope: 'public', version: 1 }, async () => ({}));
520
+ AdminRPC.Create('GetAdmin', { scope: 'admin', version: 1 }, async () => ({}));
521
+ builder
522
+ .register(PublicRPC, () => ({}), () => ({
523
+ security: [],
524
+ tags: ['public'],
525
+ }))
526
+ .register(AdminRPC, () => ({}), () => ({
527
+ security: [{ bearerAuth: [] }],
528
+ tags: ['admin'],
529
+ }));
530
+ builder.build();
531
+ const publicDoc = builder.docs.find((d) => d.name === 'GetPublic');
532
+ const adminDoc = builder.docs.find((d) => d.name === 'GetAdmin');
533
+ expect(publicDoc).toHaveProperty('security', []);
534
+ expect(publicDoc).toHaveProperty('tags', ['public']);
535
+ expect(adminDoc).toHaveProperty('security', [{ bearerAuth: [] }]);
536
+ expect(adminDoc).toHaveProperty('tags', ['admin']);
537
+ });
538
+ test('extendProcedureDoc is called for each procedure in factory', () => {
539
+ const extendFn = vi.fn(() => ({ extended: true }));
540
+ const builder = new ExpressRPCAppBuilder();
541
+ const RPC = Procedures();
542
+ RPC.Create('Method1', { scope: 'm1', version: 1 }, async () => ({}));
543
+ RPC.Create('Method2', { scope: 'm2', version: 1 }, async () => ({}));
544
+ RPC.Create('Method3', { scope: 'm3', version: 1 }, async () => ({}));
545
+ builder.register(RPC, () => ({}), extendFn);
546
+ builder.build();
547
+ expect(extendFn).toHaveBeenCalledTimes(3);
548
+ const procedureNames = extendFn.mock.calls.map((call) => call[0].procedure.name);
549
+ expect(procedureNames).toContain('Method1');
550
+ expect(procedureNames).toContain('Method2');
551
+ expect(procedureNames).toContain('Method3');
552
+ });
553
+ test('not providing extendProcedureDoc results in base documentation only', () => {
554
+ const builder = new ExpressRPCAppBuilder();
555
+ const RPC = Procedures();
556
+ RPC.Create('Test', { scope: 'test', version: 1 }, async () => ({}));
557
+ builder.register(RPC, () => ({})); // No extendProcedureDoc
558
+ builder.build();
559
+ const doc = builder.docs[0];
560
+ expect(doc.name).toBe('Test');
561
+ expect(doc.path).toBe('/test/test/1');
562
+ expect(doc.method).toBe('post');
563
+ // Should not have any extra properties
564
+ expect(Object.keys(doc)).toEqual(['kind', 'name', 'version', 'scope', 'path', 'method', 'jsonSchema']);
565
+ });
566
+ test('extendProcedureDoc can access procedure config for conditional logic', () => {
567
+ const builder = new ExpressRPCAppBuilder();
568
+ const RPC = Procedures();
569
+ RPC.Create('PublicEndpoint', { scope: 'public', version: 1 }, async () => ({}));
570
+ RPC.Create('PrivateEndpoint', { scope: 'private', version: 1 }, async () => ({}));
571
+ builder.register(RPC, () => ({}), ({ procedure }) => ({
572
+ isPublic: procedure.config.scope === 'public',
573
+ description: `This is a ${procedure.config.scope} endpoint`,
574
+ }));
575
+ builder.build();
576
+ const publicDoc = builder.docs.find((d) => d.name === 'PublicEndpoint');
577
+ const privateDoc = builder.docs.find((d) => d.name === 'PrivateEndpoint');
578
+ expect(publicDoc).toHaveProperty('isPublic', true);
579
+ expect(publicDoc).toHaveProperty('description', 'This is a public endpoint');
580
+ expect(privateDoc).toHaveProperty('isPublic', false);
581
+ expect(privateDoc).toHaveProperty('description', 'This is a private endpoint');
582
+ });
583
+ test('extendProcedureDoc can use base jsonSchema for OpenAPI-style docs', () => {
584
+ const builder = new ExpressRPCAppBuilder();
585
+ const RPC = Procedures();
586
+ const paramsSchema = v.object({ userId: v.string() });
587
+ const returnSchema = v.object({ name: v.string(), email: v.string() });
588
+ RPC.Create('GetUser', { scope: 'users', version: 1, schema: { params: paramsSchema, returnType: returnSchema } }, async () => ({ name: 'test', email: 'test@example.com' }));
589
+ builder.register(RPC, () => ({}), ({ base }) => ({
590
+ requestBody: base.jsonSchema.body
591
+ ? { content: { 'application/json': { schema: base.jsonSchema.body } } }
592
+ : undefined,
593
+ responses: {
594
+ 200: base.jsonSchema.response
595
+ ? { content: { 'application/json': { schema: base.jsonSchema.response } } }
596
+ : { description: 'Success' },
597
+ },
598
+ }));
599
+ builder.build();
600
+ const doc = builder.docs[0];
601
+ expect(doc).toHaveProperty('requestBody');
602
+ expect(doc.requestBody).toHaveProperty('content');
603
+ expect(doc).toHaveProperty('responses');
604
+ expect(doc.responses).toHaveProperty('200');
605
+ });
606
+ });
607
+ // --------------------------------------------------------------------------
608
+ // Integration Test
609
+ // --------------------------------------------------------------------------
610
+ describe('integration', () => {
611
+ test('full workflow with multiple procedure factories and different contexts', async () => {
612
+ // Create factories
613
+ const PublicRPC = Procedures();
614
+ const AuthRPC = Procedures();
615
+ // Create public procedures
616
+ PublicRPC.Create('GetVersion', { scope: ['system', 'version'], version: 1 }, async () => ({
617
+ version: '1.0.0',
618
+ }));
619
+ PublicRPC.Create('HealthCheck', { scope: 'health', version: 1 }, async () => ({
620
+ status: 'ok',
621
+ }));
622
+ // Create authenticated procedures
623
+ AuthRPC.Create('GetProfile', {
624
+ scope: ['users', 'profile'],
625
+ version: 1,
626
+ schema: { returnType: v.object({ userId: v.string(), source: v.string() }) },
627
+ }, async (ctx) => ({ userId: ctx.userId, source: ctx.source }));
628
+ AuthRPC.Create('UpdateProfile', {
629
+ scope: ['users', 'profile'],
630
+ version: 2,
631
+ schema: { params: v.object({ name: v.string() }) },
632
+ }, async (ctx, params) => ({ userId: ctx.userId, name: params.name }));
633
+ // Build app with lifecycle hooks
634
+ const events = [];
635
+ const builder = new ExpressRPCAppBuilder({
636
+ onRequestStart: () => events.push('request-start'),
637
+ onRequestEnd: () => events.push('request-end'),
638
+ onSuccess: (proc) => events.push(`success:${proc.name}`),
639
+ });
640
+ builder
641
+ .register(PublicRPC, () => ({ source: 'public' }))
642
+ .register(AuthRPC, (req) => ({
643
+ source: 'auth',
644
+ userId: req.headers['x-user-id'] || 'anonymous',
645
+ }));
646
+ const app = builder.build();
647
+ // Test public endpoints
648
+ const versionRes = await request(app).post('/system/version/get-version/1').send({});
649
+ expect(versionRes.status).toBe(200);
650
+ expect(versionRes.body).toEqual({ version: '1.0.0' });
651
+ const healthRes = await request(app).post('/health/health-check/1').send({});
652
+ expect(healthRes.status).toBe(200);
653
+ expect(healthRes.body).toEqual({ status: 'ok' });
654
+ // Test authenticated endpoints
655
+ const profileRes = await request(app)
656
+ .post('/users/profile/get-profile/1')
657
+ .set('X-User-Id', 'user-123')
658
+ .send({});
659
+ expect(profileRes.status).toBe(200);
660
+ expect(profileRes.body).toEqual({ userId: 'user-123', source: 'auth' });
661
+ const updateRes = await request(app)
662
+ .post('/users/profile/update-profile/2')
663
+ .set('X-User-Id', 'user-456')
664
+ .send({ name: 'John Doe' });
665
+ expect(updateRes.status).toBe(200);
666
+ expect(updateRes.body).toEqual({ userId: 'user-456', name: 'John Doe' });
667
+ // Verify documentation
668
+ expect(builder.docs).toHaveLength(4);
669
+ const paths = builder.docs.map((d) => d.path);
670
+ expect(paths).toContain('/system/version/get-version/1');
671
+ expect(paths).toContain('/health/health-check/1');
672
+ expect(paths).toContain('/users/profile/get-profile/1');
673
+ expect(paths).toContain('/users/profile/update-profile/2');
674
+ // Verify hooks were called
675
+ expect(events).toContain('request-start');
676
+ expect(events).toContain('success:GetVersion');
677
+ expect(events).toContain('success:HealthCheck');
678
+ expect(events).toContain('success:GetProfile');
679
+ expect(events).toContain('success:UpdateProfile');
680
+ expect(events).toContain('request-end');
681
+ });
682
+ });
683
+ });
684
+ //# sourceMappingURL=index.test.js.map