ts-procedures 5.14.0 → 5.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (277) hide show
  1. package/agent_config/claude-code/skills/ts-procedures/SKILL.md +1 -1
  2. package/agent_config/claude-code/skills/ts-procedures/api-reference.md +59 -4
  3. package/agent_config/claude-code/skills/ts-procedures/patterns.md +103 -3
  4. package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/client.md +36 -5
  5. package/agent_config/copilot/copilot-instructions.md +56 -7
  6. package/agent_config/cursor/cursorrules +56 -7
  7. package/build/client/call.d.ts +18 -9
  8. package/build/client/call.js +25 -19
  9. package/build/client/call.js.map +1 -1
  10. package/build/client/call.test.js +167 -17
  11. package/build/client/call.test.js.map +1 -1
  12. package/build/client/index.d.ts +1 -1
  13. package/build/client/index.js +18 -3
  14. package/build/client/index.js.map +1 -1
  15. package/build/client/index.test.js +104 -0
  16. package/build/client/index.test.js.map +1 -1
  17. package/build/client/resolve-options.d.ts +45 -0
  18. package/build/client/resolve-options.js +82 -0
  19. package/build/client/resolve-options.js.map +1 -0
  20. package/build/client/resolve-options.test.js +158 -0
  21. package/build/client/resolve-options.test.js.map +1 -0
  22. package/build/client/stream.d.ts +18 -9
  23. package/build/client/stream.js +24 -19
  24. package/build/client/stream.js.map +1 -1
  25. package/build/client/stream.test.js +102 -46
  26. package/build/client/stream.test.js.map +1 -1
  27. package/build/client/types.d.ts +68 -1
  28. package/build/client/types.js +1 -1
  29. package/build/codegen/bin/cli.d.ts +2 -0
  30. package/build/codegen/bin/cli.js +11 -0
  31. package/build/codegen/bin/cli.js.map +1 -1
  32. package/build/codegen/bin/cli.test.js +30 -0
  33. package/build/codegen/bin/cli.test.js.map +1 -1
  34. package/build/codegen/e2e.test.js +141 -0
  35. package/build/codegen/e2e.test.js.map +1 -1
  36. package/build/codegen/emit-client-runtime.js +3 -0
  37. package/build/codegen/emit-client-runtime.js.map +1 -1
  38. package/build/codegen/index.d.ts +1 -0
  39. package/build/codegen/index.js +1 -0
  40. package/build/codegen/index.js.map +1 -1
  41. package/build/codegen/pipeline.d.ts +1 -0
  42. package/build/codegen/pipeline.js +8 -2
  43. package/build/codegen/pipeline.js.map +1 -1
  44. package/build/codegen/pipeline.test.js +34 -1
  45. package/build/codegen/pipeline.test.js.map +1 -1
  46. package/docs/client-and-codegen.md +125 -2
  47. package/package.json +1 -1
  48. package/src/client/call.test.ts +202 -29
  49. package/src/client/call.ts +41 -28
  50. package/src/client/index.test.ts +117 -0
  51. package/src/client/index.ts +25 -8
  52. package/src/client/resolve-options.test.ts +205 -0
  53. package/src/client/resolve-options.ts +113 -0
  54. package/src/client/stream.test.ts +132 -107
  55. package/src/client/stream.ts +40 -25
  56. package/src/client/types.ts +74 -2
  57. package/src/codegen/bin/cli.test.ts +35 -0
  58. package/src/codegen/bin/cli.ts +11 -0
  59. package/src/codegen/e2e.test.ts +151 -0
  60. package/src/codegen/emit-client-runtime.ts +3 -0
  61. package/src/codegen/index.ts +2 -0
  62. package/src/codegen/pipeline.test.ts +46 -1
  63. package/src/codegen/pipeline.ts +9 -2
  64. package/src/implementations/http/README.md +11 -1
  65. package/build/src/client/call.d.ts +0 -14
  66. package/build/src/client/call.js +0 -47
  67. package/build/src/client/call.js.map +0 -1
  68. package/build/src/client/call.test.js +0 -124
  69. package/build/src/client/call.test.js.map +0 -1
  70. package/build/src/client/errors.d.ts +0 -25
  71. package/build/src/client/errors.js +0 -33
  72. package/build/src/client/errors.js.map +0 -1
  73. package/build/src/client/errors.test.d.ts +0 -1
  74. package/build/src/client/errors.test.js +0 -41
  75. package/build/src/client/errors.test.js.map +0 -1
  76. package/build/src/client/fetch-adapter.d.ts +0 -12
  77. package/build/src/client/fetch-adapter.js +0 -156
  78. package/build/src/client/fetch-adapter.js.map +0 -1
  79. package/build/src/client/fetch-adapter.test.d.ts +0 -1
  80. package/build/src/client/fetch-adapter.test.js +0 -271
  81. package/build/src/client/fetch-adapter.test.js.map +0 -1
  82. package/build/src/client/hooks.d.ts +0 -17
  83. package/build/src/client/hooks.js +0 -40
  84. package/build/src/client/hooks.js.map +0 -1
  85. package/build/src/client/hooks.test.d.ts +0 -1
  86. package/build/src/client/hooks.test.js +0 -163
  87. package/build/src/client/hooks.test.js.map +0 -1
  88. package/build/src/client/index.d.ts +0 -22
  89. package/build/src/client/index.js +0 -67
  90. package/build/src/client/index.js.map +0 -1
  91. package/build/src/client/index.test.d.ts +0 -1
  92. package/build/src/client/index.test.js +0 -231
  93. package/build/src/client/index.test.js.map +0 -1
  94. package/build/src/client/request-builder.d.ts +0 -13
  95. package/build/src/client/request-builder.js +0 -53
  96. package/build/src/client/request-builder.js.map +0 -1
  97. package/build/src/client/request-builder.test.d.ts +0 -1
  98. package/build/src/client/request-builder.test.js +0 -160
  99. package/build/src/client/request-builder.test.js.map +0 -1
  100. package/build/src/client/stream.d.ts +0 -27
  101. package/build/src/client/stream.js +0 -118
  102. package/build/src/client/stream.js.map +0 -1
  103. package/build/src/client/stream.test.d.ts +0 -1
  104. package/build/src/client/stream.test.js +0 -228
  105. package/build/src/client/stream.test.js.map +0 -1
  106. package/build/src/client/types.d.ts +0 -78
  107. package/build/src/client/types.js +0 -3
  108. package/build/src/client/types.js.map +0 -1
  109. package/build/src/codegen/bin/cli.d.ts +0 -45
  110. package/build/src/codegen/bin/cli.js +0 -246
  111. package/build/src/codegen/bin/cli.js.map +0 -1
  112. package/build/src/codegen/bin/cli.test.d.ts +0 -1
  113. package/build/src/codegen/bin/cli.test.js +0 -220
  114. package/build/src/codegen/bin/cli.test.js.map +0 -1
  115. package/build/src/codegen/constants.d.ts +0 -1
  116. package/build/src/codegen/constants.js +0 -2
  117. package/build/src/codegen/constants.js.map +0 -1
  118. package/build/src/codegen/e2e.test.d.ts +0 -1
  119. package/build/src/codegen/e2e.test.js +0 -464
  120. package/build/src/codegen/e2e.test.js.map +0 -1
  121. package/build/src/codegen/emit-client-runtime.d.ts +0 -9
  122. package/build/src/codegen/emit-client-runtime.js +0 -99
  123. package/build/src/codegen/emit-client-runtime.js.map +0 -1
  124. package/build/src/codegen/emit-client-runtime.test.d.ts +0 -1
  125. package/build/src/codegen/emit-client-runtime.test.js +0 -78
  126. package/build/src/codegen/emit-client-runtime.test.js.map +0 -1
  127. package/build/src/codegen/emit-client-types.d.ts +0 -8
  128. package/build/src/codegen/emit-client-types.js +0 -25
  129. package/build/src/codegen/emit-client-types.js.map +0 -1
  130. package/build/src/codegen/emit-client-types.test.d.ts +0 -1
  131. package/build/src/codegen/emit-client-types.test.js +0 -33
  132. package/build/src/codegen/emit-client-types.test.js.map +0 -1
  133. package/build/src/codegen/emit-errors.d.ts +0 -19
  134. package/build/src/codegen/emit-errors.js +0 -59
  135. package/build/src/codegen/emit-errors.js.map +0 -1
  136. package/build/src/codegen/emit-errors.test.d.ts +0 -1
  137. package/build/src/codegen/emit-errors.test.js +0 -175
  138. package/build/src/codegen/emit-errors.test.js.map +0 -1
  139. package/build/src/codegen/emit-index.d.ts +0 -12
  140. package/build/src/codegen/emit-index.js +0 -41
  141. package/build/src/codegen/emit-index.js.map +0 -1
  142. package/build/src/codegen/emit-index.test.d.ts +0 -1
  143. package/build/src/codegen/emit-index.test.js +0 -106
  144. package/build/src/codegen/emit-index.test.js.map +0 -1
  145. package/build/src/codegen/emit-scope.d.ts +0 -15
  146. package/build/src/codegen/emit-scope.js +0 -299
  147. package/build/src/codegen/emit-scope.js.map +0 -1
  148. package/build/src/codegen/emit-scope.test.d.ts +0 -1
  149. package/build/src/codegen/emit-scope.test.js +0 -559
  150. package/build/src/codegen/emit-scope.test.js.map +0 -1
  151. package/build/src/codegen/emit-types.d.ts +0 -43
  152. package/build/src/codegen/emit-types.js +0 -111
  153. package/build/src/codegen/emit-types.js.map +0 -1
  154. package/build/src/codegen/emit-types.test.d.ts +0 -1
  155. package/build/src/codegen/emit-types.test.js +0 -184
  156. package/build/src/codegen/emit-types.test.js.map +0 -1
  157. package/build/src/codegen/group-routes.d.ts +0 -23
  158. package/build/src/codegen/group-routes.js +0 -46
  159. package/build/src/codegen/group-routes.js.map +0 -1
  160. package/build/src/codegen/group-routes.test.d.ts +0 -1
  161. package/build/src/codegen/group-routes.test.js +0 -131
  162. package/build/src/codegen/group-routes.test.js.map +0 -1
  163. package/build/src/codegen/index.d.ts +0 -15
  164. package/build/src/codegen/index.js +0 -16
  165. package/build/src/codegen/index.js.map +0 -1
  166. package/build/src/codegen/naming.d.ts +0 -7
  167. package/build/src/codegen/naming.js +0 -21
  168. package/build/src/codegen/naming.js.map +0 -1
  169. package/build/src/codegen/naming.test.d.ts +0 -1
  170. package/build/src/codegen/naming.test.js +0 -40
  171. package/build/src/codegen/naming.test.js.map +0 -1
  172. package/build/src/codegen/pipeline.d.ts +0 -17
  173. package/build/src/codegen/pipeline.js +0 -78
  174. package/build/src/codegen/pipeline.js.map +0 -1
  175. package/build/src/codegen/pipeline.test.d.ts +0 -1
  176. package/build/src/codegen/pipeline.test.js +0 -269
  177. package/build/src/codegen/pipeline.test.js.map +0 -1
  178. package/build/src/codegen/resolve-envelope.d.ts +0 -7
  179. package/build/src/codegen/resolve-envelope.js +0 -46
  180. package/build/src/codegen/resolve-envelope.js.map +0 -1
  181. package/build/src/codegen/resolve-envelope.test.d.ts +0 -1
  182. package/build/src/codegen/resolve-envelope.test.js +0 -69
  183. package/build/src/codegen/resolve-envelope.test.js.map +0 -1
  184. package/build/src/errors.d.ts +0 -33
  185. package/build/src/errors.js +0 -91
  186. package/build/src/errors.js.map +0 -1
  187. package/build/src/errors.test.d.ts +0 -1
  188. package/build/src/errors.test.js +0 -122
  189. package/build/src/errors.test.js.map +0 -1
  190. package/build/src/exports.d.ts +0 -7
  191. package/build/src/exports.js +0 -8
  192. package/build/src/exports.js.map +0 -1
  193. package/build/src/implementations/http/doc-registry.d.ts +0 -12
  194. package/build/src/implementations/http/doc-registry.js +0 -114
  195. package/build/src/implementations/http/doc-registry.js.map +0 -1
  196. package/build/src/implementations/http/doc-registry.test.d.ts +0 -1
  197. package/build/src/implementations/http/doc-registry.test.js +0 -347
  198. package/build/src/implementations/http/doc-registry.test.js.map +0 -1
  199. package/build/src/implementations/http/express-rpc/index.d.ts +0 -94
  200. package/build/src/implementations/http/express-rpc/index.js +0 -185
  201. package/build/src/implementations/http/express-rpc/index.js.map +0 -1
  202. package/build/src/implementations/http/express-rpc/index.test.d.ts +0 -1
  203. package/build/src/implementations/http/express-rpc/index.test.js +0 -684
  204. package/build/src/implementations/http/express-rpc/index.test.js.map +0 -1
  205. package/build/src/implementations/http/express-rpc/types.d.ts +0 -11
  206. package/build/src/implementations/http/express-rpc/types.js +0 -2
  207. package/build/src/implementations/http/express-rpc/types.js.map +0 -1
  208. package/build/src/implementations/http/hono-api/index.d.ts +0 -102
  209. package/build/src/implementations/http/hono-api/index.js +0 -341
  210. package/build/src/implementations/http/hono-api/index.js.map +0 -1
  211. package/build/src/implementations/http/hono-api/index.test.d.ts +0 -1
  212. package/build/src/implementations/http/hono-api/index.test.js +0 -992
  213. package/build/src/implementations/http/hono-api/index.test.js.map +0 -1
  214. package/build/src/implementations/http/hono-api/types.d.ts +0 -13
  215. package/build/src/implementations/http/hono-api/types.js +0 -2
  216. package/build/src/implementations/http/hono-api/types.js.map +0 -1
  217. package/build/src/implementations/http/hono-rpc/index.d.ts +0 -92
  218. package/build/src/implementations/http/hono-rpc/index.js +0 -161
  219. package/build/src/implementations/http/hono-rpc/index.js.map +0 -1
  220. package/build/src/implementations/http/hono-rpc/index.test.d.ts +0 -1
  221. package/build/src/implementations/http/hono-rpc/index.test.js +0 -803
  222. package/build/src/implementations/http/hono-rpc/index.test.js.map +0 -1
  223. package/build/src/implementations/http/hono-rpc/types.d.ts +0 -11
  224. package/build/src/implementations/http/hono-rpc/types.js +0 -2
  225. package/build/src/implementations/http/hono-rpc/types.js.map +0 -1
  226. package/build/src/implementations/http/hono-stream/index.d.ts +0 -120
  227. package/build/src/implementations/http/hono-stream/index.js +0 -309
  228. package/build/src/implementations/http/hono-stream/index.js.map +0 -1
  229. package/build/src/implementations/http/hono-stream/index.test.d.ts +0 -1
  230. package/build/src/implementations/http/hono-stream/index.test.js +0 -1356
  231. package/build/src/implementations/http/hono-stream/index.test.js.map +0 -1
  232. package/build/src/implementations/http/hono-stream/types.d.ts +0 -15
  233. package/build/src/implementations/http/hono-stream/types.js +0 -2
  234. package/build/src/implementations/http/hono-stream/types.js.map +0 -1
  235. package/build/src/implementations/types.d.ts +0 -142
  236. package/build/src/implementations/types.js +0 -2
  237. package/build/src/implementations/types.js.map +0 -1
  238. package/build/src/index.d.ts +0 -165
  239. package/build/src/index.js +0 -253
  240. package/build/src/index.js.map +0 -1
  241. package/build/src/index.test.d.ts +0 -1
  242. package/build/src/index.test.js +0 -890
  243. package/build/src/index.test.js.map +0 -1
  244. package/build/src/schema/compute-schema.d.ts +0 -35
  245. package/build/src/schema/compute-schema.js +0 -41
  246. package/build/src/schema/compute-schema.js.map +0 -1
  247. package/build/src/schema/compute-schema.test.d.ts +0 -1
  248. package/build/src/schema/compute-schema.test.js +0 -107
  249. package/build/src/schema/compute-schema.test.js.map +0 -1
  250. package/build/src/schema/extract-json-schema.d.ts +0 -2
  251. package/build/src/schema/extract-json-schema.js +0 -12
  252. package/build/src/schema/extract-json-schema.js.map +0 -1
  253. package/build/src/schema/extract-json-schema.test.d.ts +0 -1
  254. package/build/src/schema/extract-json-schema.test.js +0 -23
  255. package/build/src/schema/extract-json-schema.test.js.map +0 -1
  256. package/build/src/schema/parser.d.ts +0 -28
  257. package/build/src/schema/parser.js +0 -170
  258. package/build/src/schema/parser.js.map +0 -1
  259. package/build/src/schema/parser.test.d.ts +0 -1
  260. package/build/src/schema/parser.test.js +0 -120
  261. package/build/src/schema/parser.test.js.map +0 -1
  262. package/build/src/schema/resolve-schema-lib.d.ts +0 -12
  263. package/build/src/schema/resolve-schema-lib.js +0 -11
  264. package/build/src/schema/resolve-schema-lib.js.map +0 -1
  265. package/build/src/schema/resolve-schema-lib.test.d.ts +0 -1
  266. package/build/src/schema/resolve-schema-lib.test.js +0 -17
  267. package/build/src/schema/resolve-schema-lib.test.js.map +0 -1
  268. package/build/src/schema/types.d.ts +0 -8
  269. package/build/src/schema/types.js +0 -2
  270. package/build/src/schema/types.js.map +0 -1
  271. package/build/src/stack-utils.d.ts +0 -25
  272. package/build/src/stack-utils.js +0 -95
  273. package/build/src/stack-utils.js.map +0 -1
  274. package/build/src/stack-utils.test.d.ts +0 -1
  275. package/build/src/stack-utils.test.js +0 -80
  276. package/build/src/stack-utils.test.js.map +0 -1
  277. /package/build/{src/client/call.test.d.ts → client/resolve-options.test.d.ts} +0 -0
@@ -1,992 +0,0 @@
1
- import { describe, expect, test, vi } from 'vitest';
2
- import { Hono } from 'hono';
3
- import { Type } from 'typebox';
4
- import { Procedures } from '../../../index.js';
5
- import { HonoAPIAppBuilder } from './index.js';
6
- /**
7
- * HonoAPIAppBuilder Test Suite
8
- *
9
- * Tests the REST-style Hono integration for ts-procedures.
10
- * Uses schema.input for per-channel type-safe input validation.
11
- */
12
- describe('HonoAPIAppBuilder', () => {
13
- // --------------------------------------------------------------------------
14
- // Constructor Tests
15
- // --------------------------------------------------------------------------
16
- describe('constructor', () => {
17
- test('creates default Hono app', async () => {
18
- const builder = new HonoAPIAppBuilder();
19
- const API = Procedures();
20
- API.Create('GetUser', {
21
- path: '/users/:id',
22
- method: 'get',
23
- schema: {
24
- input: {
25
- pathParams: Type.Object({ id: Type.String() }),
26
- },
27
- returnType: Type.Object({ id: Type.String() }),
28
- },
29
- }, async (ctx, { pathParams }) => ({ id: pathParams.id }));
30
- builder.register(API, () => ({ userId: '123' }));
31
- const app = await builder.build();
32
- const res = await app.request('/users/abc', { method: 'GET' });
33
- expect(res.status).toBe(200);
34
- const body = await res.json();
35
- expect(body).toEqual({ id: 'abc' });
36
- });
37
- test('uses provided Hono app', async () => {
38
- const customApp = new Hono();
39
- customApp.get('/custom', (c) => c.json({ custom: true }));
40
- const builder = new HonoAPIAppBuilder({ app: customApp });
41
- const API = Procedures();
42
- API.Create('Health', { path: '/health', method: 'get' }, async () => ({ ok: true }));
43
- builder.register(API, () => ({}));
44
- const app = await builder.build();
45
- // Custom route still works
46
- const customRes = await app.request('/custom');
47
- expect(customRes.status).toBe(200);
48
- expect(await customRes.json()).toEqual({ custom: true });
49
- // API route works
50
- const apiRes = await app.request('/health');
51
- expect(apiRes.status).toBe(200);
52
- });
53
- test('handles empty config', () => {
54
- const builder = new HonoAPIAppBuilder({});
55
- expect(builder.app).toBeDefined();
56
- expect(builder.docs).toEqual([]);
57
- });
58
- });
59
- // --------------------------------------------------------------------------
60
- // HTTP Methods
61
- // --------------------------------------------------------------------------
62
- describe('HTTP methods', () => {
63
- test('GET endpoint', async () => {
64
- const builder = new HonoAPIAppBuilder();
65
- const API = Procedures();
66
- API.Create('ListUsers', { path: '/users', method: 'get' }, async () => ({ users: [] }));
67
- builder.register(API, () => ({}));
68
- const app = await builder.build();
69
- const res = await app.request('/users');
70
- expect(res.status).toBe(200);
71
- expect(await res.json()).toEqual({ users: [] });
72
- });
73
- test('POST endpoint returns 201 by default', async () => {
74
- const builder = new HonoAPIAppBuilder();
75
- const API = Procedures();
76
- API.Create('CreateUser', {
77
- path: '/users',
78
- method: 'post',
79
- schema: {
80
- input: {
81
- body: Type.Object({ name: Type.String() }),
82
- },
83
- },
84
- }, async (ctx, { body }) => ({ id: '123', name: body.name }));
85
- builder.register(API, () => ({}));
86
- const app = await builder.build();
87
- const res = await app.request('/users', {
88
- method: 'POST',
89
- headers: { 'Content-Type': 'application/json' },
90
- body: JSON.stringify({ name: 'John' }),
91
- });
92
- expect(res.status).toBe(201);
93
- expect(await res.json()).toEqual({ id: '123', name: 'John' });
94
- });
95
- test('PUT endpoint returns 200 by default', async () => {
96
- const builder = new HonoAPIAppBuilder();
97
- const API = Procedures();
98
- API.Create('UpdateUser', {
99
- path: '/users/:id',
100
- method: 'put',
101
- schema: {
102
- input: {
103
- pathParams: Type.Object({ id: Type.String() }),
104
- body: Type.Object({ name: Type.String() }),
105
- },
106
- },
107
- }, async (ctx, { pathParams, body }) => ({ id: pathParams.id, name: body.name }));
108
- builder.register(API, () => ({}));
109
- const app = await builder.build();
110
- const res = await app.request('/users/abc', {
111
- method: 'PUT',
112
- headers: { 'Content-Type': 'application/json' },
113
- body: JSON.stringify({ name: 'Jane' }),
114
- });
115
- expect(res.status).toBe(200);
116
- expect(await res.json()).toEqual({ id: 'abc', name: 'Jane' });
117
- });
118
- test('PATCH endpoint', async () => {
119
- const builder = new HonoAPIAppBuilder();
120
- const API = Procedures();
121
- API.Create('PatchUser', {
122
- path: '/users/:id',
123
- method: 'patch',
124
- schema: {
125
- input: {
126
- pathParams: Type.Object({ id: Type.String() }),
127
- body: Type.Object({ name: Type.Optional(Type.String()) }),
128
- },
129
- },
130
- }, async (ctx, { pathParams, body }) => ({ id: pathParams.id, ...body }));
131
- builder.register(API, () => ({}));
132
- const app = await builder.build();
133
- const res = await app.request('/users/abc', {
134
- method: 'PATCH',
135
- headers: { 'Content-Type': 'application/json' },
136
- body: JSON.stringify({ name: 'Updated' }),
137
- });
138
- expect(res.status).toBe(200);
139
- expect(await res.json()).toEqual({ id: 'abc', name: 'Updated' });
140
- });
141
- test('DELETE endpoint returns 204 by default', async () => {
142
- const builder = new HonoAPIAppBuilder();
143
- const API = Procedures();
144
- API.Create('DeleteUser', {
145
- path: '/users/:id',
146
- method: 'delete',
147
- schema: {
148
- input: {
149
- pathParams: Type.Object({ id: Type.String() }),
150
- },
151
- },
152
- }, async (ctx, { pathParams }) => undefined);
153
- builder.register(API, () => ({}));
154
- const app = await builder.build();
155
- const res = await app.request('/users/abc', { method: 'DELETE' });
156
- expect(res.status).toBe(204);
157
- // 204 responses have no body
158
- expect(await res.text()).toBe('');
159
- });
160
- test('custom successStatus overrides default', async () => {
161
- const builder = new HonoAPIAppBuilder();
162
- const API = Procedures();
163
- API.Create('CreateUser', {
164
- path: '/users',
165
- method: 'post',
166
- successStatus: 200, // override default 201
167
- }, async () => ({ id: '123' }));
168
- builder.register(API, () => ({}));
169
- const app = await builder.build();
170
- const res = await app.request('/users', {
171
- method: 'POST',
172
- headers: { 'Content-Type': 'application/json' },
173
- body: JSON.stringify({}),
174
- });
175
- expect(res.status).toBe(200);
176
- });
177
- test('wrong HTTP method returns 404', async () => {
178
- const builder = new HonoAPIAppBuilder();
179
- const API = Procedures();
180
- API.Create('GetUser', { path: '/users', method: 'get' }, async () => ({ ok: true }));
181
- builder.register(API, () => ({}));
182
- const app = await builder.build();
183
- // POST to a GET-only endpoint
184
- const res = await app.request('/users', {
185
- method: 'POST',
186
- headers: { 'Content-Type': 'application/json' },
187
- body: JSON.stringify({}),
188
- });
189
- expect(res.status).toBe(404);
190
- });
191
- });
192
- // --------------------------------------------------------------------------
193
- // schema.input — Multi-Channel Input
194
- // --------------------------------------------------------------------------
195
- describe('schema.input — multi-channel input', () => {
196
- test('pathParams extraction and validation', async () => {
197
- const builder = new HonoAPIAppBuilder();
198
- const API = Procedures();
199
- API.Create('GetUser', {
200
- path: '/users/:id',
201
- method: 'get',
202
- schema: {
203
- input: {
204
- pathParams: Type.Object({ id: Type.String() }),
205
- },
206
- },
207
- }, async (ctx, { pathParams }) => ({ id: pathParams.id }));
208
- builder.register(API, () => ({}));
209
- const app = await builder.build();
210
- const res = await app.request('/users/user-123');
211
- expect(res.status).toBe(200);
212
- expect(await res.json()).toEqual({ id: 'user-123' });
213
- });
214
- test('query params extraction and validation', async () => {
215
- const builder = new HonoAPIAppBuilder();
216
- const API = Procedures();
217
- API.Create('SearchUsers', {
218
- path: '/users',
219
- method: 'get',
220
- schema: {
221
- input: {
222
- query: Type.Object({
223
- page: Type.Number(),
224
- limit: Type.Number(),
225
- }),
226
- },
227
- },
228
- }, async (ctx, { query }) => ({ page: query.page, limit: query.limit }));
229
- builder.register(API, () => ({}));
230
- const app = await builder.build();
231
- // AJV coerceTypes converts string "2" to number 2
232
- const res = await app.request('/users?page=2&limit=10');
233
- expect(res.status).toBe(200);
234
- expect(await res.json()).toEqual({ page: 2, limit: 10 });
235
- });
236
- test('body extraction and validation', async () => {
237
- const builder = new HonoAPIAppBuilder();
238
- const API = Procedures();
239
- API.Create('CreateUser', {
240
- path: '/users',
241
- method: 'post',
242
- schema: {
243
- input: {
244
- body: Type.Object({
245
- name: Type.String(),
246
- email: Type.String(),
247
- }),
248
- },
249
- },
250
- }, async (ctx, { body }) => ({ id: '1', name: body.name, email: body.email }));
251
- builder.register(API, () => ({}));
252
- const app = await builder.build();
253
- const res = await app.request('/users', {
254
- method: 'POST',
255
- headers: { 'Content-Type': 'application/json' },
256
- body: JSON.stringify({ name: 'John', email: 'john@example.com' }),
257
- });
258
- expect(res.status).toBe(201);
259
- expect(await res.json()).toEqual({ id: '1', name: 'John', email: 'john@example.com' });
260
- });
261
- test('headers extraction and validation', async () => {
262
- const builder = new HonoAPIAppBuilder();
263
- const API = Procedures();
264
- API.Create('WebhookReceiver', {
265
- path: '/webhooks',
266
- method: 'post',
267
- schema: {
268
- input: {
269
- headers: Type.Object({ 'x-webhook-secret': Type.String() }),
270
- body: Type.Object({ event: Type.String() }),
271
- },
272
- },
273
- }, async (ctx, { headers, body }) => ({
274
- received: body.event,
275
- secret: headers['x-webhook-secret'],
276
- }));
277
- builder.register(API, () => ({}));
278
- const app = await builder.build();
279
- const res = await app.request('/webhooks', {
280
- method: 'POST',
281
- headers: {
282
- 'Content-Type': 'application/json',
283
- 'X-Webhook-Secret': 'my-secret',
284
- },
285
- body: JSON.stringify({ event: 'user.created' }),
286
- });
287
- expect(res.status).toBe(201);
288
- const body = await res.json();
289
- expect(body.received).toBe('user.created');
290
- expect(body.secret).toBe('my-secret');
291
- });
292
- test('combined: pathParams + query + body', async () => {
293
- const builder = new HonoAPIAppBuilder();
294
- const API = Procedures();
295
- API.Create('UpdateUserField', {
296
- path: '/users/:id',
297
- method: 'put',
298
- schema: {
299
- input: {
300
- pathParams: Type.Object({ id: Type.String() }),
301
- query: Type.Object({ notify: Type.Optional(Type.Boolean()) }),
302
- body: Type.Object({ field: Type.String(), value: Type.String() }),
303
- },
304
- },
305
- }, async (ctx, { pathParams, query, body }) => ({
306
- userId: pathParams.id,
307
- updated: { [body.field]: body.value },
308
- notified: query.notify ?? false,
309
- }));
310
- builder.register(API, () => ({}));
311
- const app = await builder.build();
312
- const res = await app.request('/users/u-42?notify=true', {
313
- method: 'PUT',
314
- headers: { 'Content-Type': 'application/json' },
315
- body: JSON.stringify({ field: 'name', value: 'Jane' }),
316
- });
317
- expect(res.status).toBe(200);
318
- expect(await res.json()).toEqual({
319
- userId: 'u-42',
320
- updated: { name: 'Jane' },
321
- notified: true,
322
- });
323
- });
324
- test('coerceTypes converts query string values to schema types', async () => {
325
- const builder = new HonoAPIAppBuilder();
326
- const API = Procedures();
327
- API.Create('Search', {
328
- path: '/items',
329
- method: 'get',
330
- schema: {
331
- input: {
332
- query: Type.Object({
333
- page: Type.Number(),
334
- active: Type.Boolean(),
335
- }),
336
- },
337
- },
338
- }, async (ctx, { query }) => ({
339
- page: query.page,
340
- active: query.active,
341
- pageType: typeof query.page,
342
- activeType: typeof query.active,
343
- }));
344
- builder.register(API, () => ({}));
345
- const app = await builder.build();
346
- const res = await app.request('/items?page=3&active=true');
347
- expect(res.status).toBe(200);
348
- const body = await res.json();
349
- expect(body.page).toBe(3);
350
- expect(body.active).toBe(true);
351
- expect(body.pageType).toBe('number');
352
- expect(body.activeType).toBe('boolean');
353
- });
354
- test('removeAdditional strips extra fields from body', async () => {
355
- const builder = new HonoAPIAppBuilder();
356
- const API = Procedures();
357
- API.Create('CreateItem', {
358
- path: '/items',
359
- method: 'post',
360
- schema: {
361
- input: {
362
- body: Type.Object({ name: Type.String() }, { additionalProperties: false }),
363
- },
364
- },
365
- }, async (ctx, { body }) => body);
366
- builder.register(API, () => ({}));
367
- const app = await builder.build();
368
- const res = await app.request('/items', {
369
- method: 'POST',
370
- headers: { 'Content-Type': 'application/json' },
371
- body: JSON.stringify({ name: 'Widget', extraField: 'should-be-stripped', anotherOne: 42 }),
372
- });
373
- expect(res.status).toBe(201);
374
- const body = await res.json();
375
- // AJV removeAdditional strips properties not in schema when additionalProperties: false
376
- expect(body).toEqual({ name: 'Widget' });
377
- expect(body).not.toHaveProperty('extraField');
378
- expect(body).not.toHaveProperty('anotherOne');
379
- });
380
- test('removeAdditional strips non-declared headers', async () => {
381
- const builder = new HonoAPIAppBuilder();
382
- const API = Procedures();
383
- API.Create('Webhook', {
384
- path: '/hook',
385
- method: 'post',
386
- schema: {
387
- input: {
388
- headers: Type.Object({ 'x-signature': Type.String() }, { additionalProperties: false }),
389
- body: Type.Object({ event: Type.String() }),
390
- },
391
- },
392
- }, async (ctx, { headers }) => ({ keys: Object.keys(headers) }));
393
- builder.register(API, () => ({}));
394
- const app = await builder.build();
395
- const res = await app.request('/hook', {
396
- method: 'POST',
397
- headers: {
398
- 'Content-Type': 'application/json',
399
- 'X-Signature': 'abc123',
400
- 'X-Other-Header': 'should-be-stripped',
401
- },
402
- body: JSON.stringify({ event: 'test' }),
403
- });
404
- expect(res.status).toBe(201);
405
- const body = await res.json();
406
- // Only x-signature should remain after removeAdditional
407
- expect(body.keys).toEqual(['x-signature']);
408
- });
409
- test('validation error includes channel name', async () => {
410
- const builder = new HonoAPIAppBuilder();
411
- const API = Procedures();
412
- API.Create('CreateItem', {
413
- path: '/items',
414
- method: 'post',
415
- schema: {
416
- input: {
417
- body: Type.Object({
418
- name: Type.String(),
419
- price: Type.Number(),
420
- }),
421
- },
422
- },
423
- }, async (ctx, { body }) => body);
424
- builder.register(API, () => ({}));
425
- const app = await builder.build();
426
- const res = await app.request('/items', {
427
- method: 'POST',
428
- headers: { 'Content-Type': 'application/json' },
429
- body: JSON.stringify({ name: 'Widget' }), // missing required 'price'
430
- });
431
- expect(res.status).toBe(500);
432
- const body = await res.json();
433
- expect(body.error).toContain('input.body');
434
- });
435
- });
436
- // --------------------------------------------------------------------------
437
- // schema.params — Flat Input (backwards-compatible path)
438
- // --------------------------------------------------------------------------
439
- describe('schema.params — flat input', () => {
440
- test('POST with schema.params reads from body', async () => {
441
- const builder = new HonoAPIAppBuilder();
442
- const API = Procedures();
443
- API.Create('CreateItem', {
444
- path: '/items',
445
- method: 'post',
446
- schema: { params: Type.Object({ name: Type.String() }) },
447
- }, async (ctx, params) => ({ name: params.name }));
448
- builder.register(API, () => ({}));
449
- const app = await builder.build();
450
- const res = await app.request('/items', {
451
- method: 'POST',
452
- headers: { 'Content-Type': 'application/json' },
453
- body: JSON.stringify({ name: 'Widget' }),
454
- });
455
- expect(res.status).toBe(201);
456
- expect(await res.json()).toEqual({ name: 'Widget' });
457
- });
458
- test('GET with schema.params reads from query', async () => {
459
- const builder = new HonoAPIAppBuilder();
460
- const API = Procedures();
461
- API.Create('SearchItems', {
462
- path: '/items',
463
- method: 'get',
464
- schema: {
465
- params: Type.Object({ q: Type.String() }),
466
- },
467
- }, async (ctx, params) => ({ query: params.q }));
468
- builder.register(API, () => ({}));
469
- const app = await builder.build();
470
- const res = await app.request('/items?q=widget');
471
- expect(res.status).toBe(200);
472
- expect(await res.json()).toEqual({ query: 'widget' });
473
- });
474
- });
475
- // --------------------------------------------------------------------------
476
- // pathPrefix Option
477
- // --------------------------------------------------------------------------
478
- describe('pathPrefix', () => {
479
- test('prepends prefix to all routes', async () => {
480
- const builder = new HonoAPIAppBuilder({ pathPrefix: '/api/v1' });
481
- const API = Procedures();
482
- API.Create('Health', { path: '/health', method: 'get' }, async () => ({ ok: true }));
483
- builder.register(API, () => ({}));
484
- const app = await builder.build();
485
- const res = await app.request('/api/v1/health');
486
- expect(res.status).toBe(200);
487
- // Without prefix → 404
488
- const res2 = await app.request('/health');
489
- expect(res2.status).toBe(404);
490
- });
491
- test('pathPrefix normalizes missing leading slash', async () => {
492
- const builder = new HonoAPIAppBuilder({ pathPrefix: 'api' });
493
- const API = Procedures();
494
- API.Create('Health', { path: '/health', method: 'get' }, async () => ({ ok: true }));
495
- builder.register(API, () => ({}));
496
- const app = await builder.build();
497
- const res = await app.request('/api/health');
498
- expect(res.status).toBe(200);
499
- });
500
- test('pathPrefix appears in docs fullPath', async () => {
501
- const builder = new HonoAPIAppBuilder({ pathPrefix: '/api' });
502
- const API = Procedures();
503
- API.Create('GetUser', { path: '/users/:id', method: 'get' }, async () => ({}));
504
- builder.register(API, () => ({}));
505
- await builder.build();
506
- expect(builder.docs[0].fullPath).toBe('/api/users/:id');
507
- expect(builder.docs[0].path).toBe('/users/:id');
508
- });
509
- });
510
- // --------------------------------------------------------------------------
511
- // Lifecycle Hooks
512
- // --------------------------------------------------------------------------
513
- describe('lifecycle hooks', () => {
514
- test('onRequestStart and onRequestEnd are called', async () => {
515
- const order = [];
516
- const builder = new HonoAPIAppBuilder({
517
- onRequestStart: () => order.push('start'),
518
- onRequestEnd: () => order.push('end'),
519
- });
520
- const API = Procedures();
521
- API.Create('Test', { path: '/test', method: 'get' }, async () => {
522
- order.push('handler');
523
- return { ok: true };
524
- });
525
- builder.register(API, () => ({}));
526
- const app = await builder.build();
527
- await app.request('/test');
528
- expect(order).toEqual(['start', 'handler', 'end']);
529
- });
530
- test('onSuccess is called on successful execution', async () => {
531
- const onSuccess = vi.fn();
532
- const builder = new HonoAPIAppBuilder({ onSuccess });
533
- const API = Procedures();
534
- API.Create('Test', { path: '/test', method: 'get' }, async () => ({ ok: true }));
535
- builder.register(API, () => ({}));
536
- const app = await builder.build();
537
- await app.request('/test');
538
- expect(onSuccess).toHaveBeenCalledTimes(1);
539
- expect(onSuccess.mock.calls[0][0]).toHaveProperty('name', 'Test');
540
- });
541
- test('onSuccess is NOT called when handler throws', async () => {
542
- const onSuccess = vi.fn();
543
- const builder = new HonoAPIAppBuilder({ onSuccess });
544
- const API = Procedures();
545
- API.Create('Test', { path: '/test', method: 'get' }, async () => {
546
- throw new Error('oops');
547
- });
548
- builder.register(API, () => ({}));
549
- const app = await builder.build();
550
- await app.request('/test');
551
- expect(onSuccess).not.toHaveBeenCalled();
552
- });
553
- });
554
- // --------------------------------------------------------------------------
555
- // Error Handling
556
- // --------------------------------------------------------------------------
557
- describe('error handling', () => {
558
- test('custom error handler receives procedure, context, and error', async () => {
559
- const errorHandler = vi.fn((procedure, c, error) => {
560
- return c.json({ customError: error.message }, 400);
561
- });
562
- const builder = new HonoAPIAppBuilder({ onError: errorHandler });
563
- const API = Procedures();
564
- API.Create('Test', { path: '/test', method: 'get' }, async () => {
565
- throw new Error('Test error');
566
- });
567
- builder.register(API, () => ({}));
568
- const app = await builder.build();
569
- const res = await app.request('/test');
570
- expect(errorHandler).toHaveBeenCalledTimes(1);
571
- expect(res.status).toBe(400);
572
- const body = await res.json();
573
- expect(body.customError).toContain('Test error');
574
- });
575
- test('default error handling returns 500 with message', async () => {
576
- const builder = new HonoAPIAppBuilder();
577
- const API = Procedures();
578
- API.Create('Test', { path: '/test', method: 'get' }, async () => {
579
- throw new Error('Something broke');
580
- });
581
- builder.register(API, () => ({}));
582
- const app = await builder.build();
583
- const res = await app.request('/test');
584
- expect(res.status).toBe(500);
585
- const body = await res.json();
586
- expect(body.error).toContain('Something broke');
587
- });
588
- });
589
- // --------------------------------------------------------------------------
590
- // register() Method
591
- // --------------------------------------------------------------------------
592
- describe('register() method', () => {
593
- test('returns this for method chaining', () => {
594
- const builder = new HonoAPIAppBuilder();
595
- const API1 = Procedures();
596
- const API2 = Procedures();
597
- const result = builder.register(API1, () => ({})).register(API2, () => ({}));
598
- expect(result).toBe(builder);
599
- });
600
- test('supports multiple factories with different contexts', async () => {
601
- const builder = new HonoAPIAppBuilder();
602
- const PublicAPI = Procedures();
603
- const AuthAPI = Procedures();
604
- PublicAPI.Create('GetHealth', { path: '/health', method: 'get' }, async (ctx) => ({ public: ctx.public }));
605
- AuthAPI.Create('GetProfile', { path: '/profile', method: 'get' }, async (ctx) => ({ userId: ctx.userId }));
606
- builder
607
- .register(PublicAPI, () => ({ public: true }))
608
- .register(AuthAPI, (c) => ({ userId: c.req.header('x-user-id') || 'anon' }));
609
- const app = await builder.build();
610
- const healthRes = await app.request('/health');
611
- expect(await healthRes.json()).toEqual({ public: true });
612
- const profileRes = await app.request('/profile', {
613
- headers: { 'X-User-Id': 'user-42' },
614
- });
615
- expect(await profileRes.json()).toEqual({ userId: 'user-42' });
616
- });
617
- test('factoryContext can be a static object', async () => {
618
- const builder = new HonoAPIAppBuilder();
619
- const API = Procedures();
620
- API.Create('GetId', { path: '/id', method: 'get' }, async (ctx) => ({ id: ctx.requestId }));
621
- builder.register(API, { requestId: 'static-123' });
622
- const app = await builder.build();
623
- const res = await app.request('/id');
624
- expect(await res.json()).toEqual({ id: 'static-123' });
625
- });
626
- test('factoryContext can be async', async () => {
627
- const builder = new HonoAPIAppBuilder();
628
- const API = Procedures();
629
- API.Create('GetId', { path: '/id', method: 'get' }, async (ctx) => ({ id: ctx.requestId }));
630
- builder.register(API, async () => ({ requestId: 'async-456' }));
631
- const app = await builder.build();
632
- const res = await app.request('/id');
633
- expect(await res.json()).toEqual({ id: 'async-456' });
634
- });
635
- });
636
- // --------------------------------------------------------------------------
637
- // Path Param Consistency Validation
638
- // --------------------------------------------------------------------------
639
- describe('path param consistency validation', () => {
640
- test('throws when path has params but schema.input.pathParams is missing', async () => {
641
- const builder = new HonoAPIAppBuilder();
642
- const API = Procedures();
643
- API.Create('GetUser', {
644
- path: '/users/:id',
645
- method: 'get',
646
- schema: {
647
- input: {
648
- query: Type.Object({ include: Type.Optional(Type.String()) }),
649
- },
650
- },
651
- }, async () => ({}));
652
- builder.register(API, () => ({}));
653
- await expect(builder.build()).rejects.toThrow(/pathParams is not defined/);
654
- });
655
- test('throws when schema.input.pathParams defined but path has no params', async () => {
656
- const builder = new HonoAPIAppBuilder();
657
- const API = Procedures();
658
- API.Create('ListUsers', {
659
- path: '/users',
660
- method: 'get',
661
- schema: {
662
- input: {
663
- pathParams: Type.Object({ id: Type.String() }),
664
- },
665
- },
666
- }, async () => ({}));
667
- builder.register(API, () => ({}));
668
- await expect(builder.build()).rejects.toThrow(/has no path parameters/);
669
- });
670
- test('no error when path has params and pathParams schema matches', async () => {
671
- const builder = new HonoAPIAppBuilder();
672
- const API = Procedures();
673
- API.Create('GetUser', {
674
- path: '/users/:id',
675
- method: 'get',
676
- schema: {
677
- input: {
678
- pathParams: Type.Object({ id: Type.String() }),
679
- },
680
- },
681
- }, async (ctx, { pathParams }) => ({ id: pathParams.id }));
682
- builder.register(API, () => ({}));
683
- await expect(builder.build()).resolves.toBeDefined();
684
- });
685
- test('throws when pathParams schema keys do not match path param names', async () => {
686
- const builder = new HonoAPIAppBuilder();
687
- const API = Procedures();
688
- // Path has :id but schema declares userId
689
- API.Create('GetUser', {
690
- path: '/users/:id',
691
- method: 'get',
692
- schema: {
693
- input: {
694
- pathParams: Type.Object({ userId: Type.String() }),
695
- },
696
- },
697
- }, async () => ({}));
698
- builder.register(API, () => ({}));
699
- await expect(builder.build()).rejects.toThrow(/Path param mismatch/);
700
- });
701
- test('throws when path has multiple params and schema is missing one', async () => {
702
- const builder = new HonoAPIAppBuilder();
703
- const API = Procedures();
704
- API.Create('GetComment', {
705
- path: '/users/:userId/posts/:postId/comments/:commentId',
706
- method: 'get',
707
- schema: {
708
- input: {
709
- // Missing commentId
710
- pathParams: Type.Object({ userId: Type.String(), postId: Type.String() }),
711
- },
712
- },
713
- }, async () => ({}));
714
- builder.register(API, () => ({}));
715
- await expect(builder.build()).rejects.toThrow(/commentId/);
716
- });
717
- test('no validation when schema.input is not used', async () => {
718
- const builder = new HonoAPIAppBuilder();
719
- const API = Procedures();
720
- // Using schema.params (flat mode) with path params — no consistency check
721
- API.Create('GetUser', {
722
- path: '/users/:id',
723
- method: 'get',
724
- }, async () => ({ ok: true }));
725
- builder.register(API, () => ({}));
726
- await expect(builder.build()).resolves.toBeDefined();
727
- });
728
- });
729
- // --------------------------------------------------------------------------
730
- // Route Documentation
731
- // --------------------------------------------------------------------------
732
- describe('route documentation', () => {
733
- test('generates docs with per-channel jsonSchema from schema.input', async () => {
734
- const builder = new HonoAPIAppBuilder();
735
- const API = Procedures();
736
- API.Create('UpdateUser', {
737
- path: '/users/:id',
738
- method: 'put',
739
- schema: {
740
- input: {
741
- pathParams: Type.Object({ id: Type.String() }),
742
- body: Type.Object({ name: Type.String() }),
743
- },
744
- returnType: Type.Object({ ok: Type.Boolean() }),
745
- },
746
- }, async () => ({ ok: true }));
747
- builder.register(API, () => ({}));
748
- await builder.build();
749
- const doc = builder.docs[0];
750
- expect(doc.name).toBe('UpdateUser');
751
- expect(doc.path).toBe('/users/:id');
752
- expect(doc.method).toBe('put');
753
- expect(doc.fullPath).toBe('/users/:id');
754
- expect(doc.jsonSchema.pathParams).toBeDefined();
755
- expect(doc.jsonSchema.body).toBeDefined();
756
- expect(doc.jsonSchema.response).toBeDefined();
757
- expect(doc.jsonSchema.query).toBeUndefined();
758
- });
759
- test('generates docs from schema.params (flat mode)', async () => {
760
- const builder = new HonoAPIAppBuilder();
761
- const API = Procedures();
762
- API.Create('CreateItem', {
763
- path: '/items',
764
- method: 'post',
765
- schema: {
766
- params: Type.Object({ name: Type.String() }),
767
- returnType: Type.Object({ id: Type.String() }),
768
- },
769
- }, async (ctx, params) => ({ id: '1' }));
770
- builder.register(API, () => ({}));
771
- await builder.build();
772
- const doc = builder.docs[0];
773
- // POST with schema.params → body in docs
774
- expect(doc.jsonSchema.body).toBeDefined();
775
- expect(doc.jsonSchema.response).toBeDefined();
776
- });
777
- test('extendProcedureDoc adds custom properties', async () => {
778
- const builder = new HonoAPIAppBuilder();
779
- const API = Procedures();
780
- API.Create('GetUser', { path: '/users/:id', method: 'get' }, async () => ({}));
781
- builder.register(API, () => ({}), ({ base, procedure }) => ({
782
- tags: ['users'],
783
- summary: `Get user by ID`,
784
- operationId: procedure.name,
785
- }));
786
- await builder.build();
787
- const doc = builder.docs[0];
788
- expect(doc).toHaveProperty('tags', ['users']);
789
- expect(doc).toHaveProperty('summary', 'Get user by ID');
790
- expect(doc).toHaveProperty('operationId', 'GetUser');
791
- });
792
- test('base properties take precedence over extended properties', async () => {
793
- const builder = new HonoAPIAppBuilder();
794
- const API = Procedures();
795
- API.Create('Test', { path: '/test', method: 'get' }, async () => ({}));
796
- builder.register(API, () => ({}), () => ({
797
- name: 'Overridden',
798
- method: 'post',
799
- customField: 'custom-value',
800
- }));
801
- await builder.build();
802
- const doc = builder.docs[0];
803
- expect(doc.name).toBe('Test');
804
- expect(doc.method).toBe('get');
805
- expect(doc).toHaveProperty('customField', 'custom-value');
806
- });
807
- test("doc.kind is 'api'", async () => {
808
- const builder = new HonoAPIAppBuilder();
809
- const API = Procedures();
810
- API.Create('GetUser', { path: '/users/:id', method: 'get' }, async () => ({}));
811
- builder.register(API, () => ({}));
812
- await builder.build();
813
- const doc = builder.docs[0];
814
- expect(doc.kind).toBe('api');
815
- });
816
- });
817
- // --------------------------------------------------------------------------
818
- // Query Parser Override
819
- // --------------------------------------------------------------------------
820
- describe('query parser', () => {
821
- test('custom queryParser is used', async () => {
822
- const customParser = vi.fn((qs) => {
823
- const params = new URLSearchParams(qs);
824
- const result = {};
825
- for (const [key, value] of params) {
826
- result[key] = value.toUpperCase(); // custom behavior
827
- }
828
- return result;
829
- });
830
- const builder = new HonoAPIAppBuilder({ queryParser: customParser });
831
- const API = Procedures();
832
- API.Create('Search', {
833
- path: '/search',
834
- method: 'get',
835
- schema: {
836
- input: {
837
- query: Type.Object({ q: Type.String() }),
838
- },
839
- },
840
- }, async (ctx, { query }) => ({ query: query.q }));
841
- builder.register(API, () => ({}));
842
- const app = await builder.build();
843
- const res = await app.request('/search?q=hello');
844
- expect(res.status).toBe(200);
845
- expect(await res.json()).toEqual({ query: 'HELLO' });
846
- expect(customParser).toHaveBeenCalledWith('q=hello');
847
- });
848
- });
849
- // --------------------------------------------------------------------------
850
- // Core schema.input Mutual Exclusivity
851
- // --------------------------------------------------------------------------
852
- describe('core schema.input mutual exclusivity', () => {
853
- test('throws when both schema.params and schema.input are defined', () => {
854
- const API = Procedures();
855
- expect(() => {
856
- API.Create('Bad', {
857
- path: '/bad',
858
- method: 'get',
859
- schema: {
860
- params: Type.Object({ a: Type.String() }),
861
- input: {
862
- query: Type.Object({ b: Type.String() }),
863
- },
864
- },
865
- }, async () => ({}));
866
- }).toThrow(/mutually exclusive/);
867
- });
868
- });
869
- // --------------------------------------------------------------------------
870
- // Integration Test
871
- // --------------------------------------------------------------------------
872
- describe('integration', () => {
873
- test('full REST API with multiple endpoints and contexts', async () => {
874
- const PublicAPI = Procedures();
875
- const AuthAPI = Procedures();
876
- PublicAPI.Create('ListProducts', {
877
- path: '/products',
878
- method: 'get',
879
- schema: {
880
- input: {
881
- query: Type.Object({
882
- page: Type.Optional(Type.Number({ default: 1 })),
883
- }),
884
- },
885
- returnType: Type.Object({ products: Type.Array(Type.String()), page: Type.Number() }),
886
- },
887
- }, async (ctx, { query }) => ({
888
- products: ['Widget', 'Gadget'],
889
- page: query.page ?? 1,
890
- }));
891
- AuthAPI.Create('GetProduct', {
892
- path: '/products/:id',
893
- method: 'get',
894
- schema: {
895
- input: {
896
- pathParams: Type.Object({ id: Type.String() }),
897
- },
898
- returnType: Type.Object({ id: Type.String(), viewer: Type.String() }),
899
- },
900
- }, async (ctx, { pathParams }) => ({
901
- id: pathParams.id,
902
- viewer: ctx.userId,
903
- }));
904
- AuthAPI.Create('CreateProduct', {
905
- path: '/products',
906
- method: 'post',
907
- schema: {
908
- input: {
909
- body: Type.Object({
910
- name: Type.String(),
911
- price: Type.Number(),
912
- }),
913
- },
914
- returnType: Type.Object({ id: Type.String(), name: Type.String(), createdBy: Type.String() }),
915
- },
916
- }, async (ctx, { body }) => ({
917
- id: 'prod-new',
918
- name: body.name,
919
- createdBy: ctx.userId,
920
- }));
921
- AuthAPI.Create('DeleteProduct', {
922
- path: '/products/:id',
923
- method: 'delete',
924
- schema: {
925
- input: {
926
- pathParams: Type.Object({ id: Type.String() }),
927
- },
928
- },
929
- }, async (ctx, { pathParams }) => undefined);
930
- const events = [];
931
- const builder = new HonoAPIAppBuilder({
932
- pathPrefix: '/api',
933
- onRequestStart: () => events.push('request-start'),
934
- onRequestEnd: () => events.push('request-end'),
935
- onSuccess: (proc) => events.push(`success:${proc.name}`),
936
- });
937
- builder
938
- .register(PublicAPI, () => ({ source: 'public' }))
939
- .register(AuthAPI, (c) => ({
940
- source: 'auth',
941
- userId: c.req.header('x-user-id') || 'anon',
942
- }));
943
- const app = await builder.build();
944
- // GET /api/products?page=2
945
- const listRes = await app.request('/api/products?page=2');
946
- expect(listRes.status).toBe(200);
947
- expect(await listRes.json()).toEqual({ products: ['Widget', 'Gadget'], page: 2 });
948
- // GET /api/products/prod-1
949
- const getRes = await app.request('/api/products/prod-1', {
950
- headers: { 'X-User-Id': 'user-42' },
951
- });
952
- expect(getRes.status).toBe(200);
953
- expect(await getRes.json()).toEqual({ id: 'prod-1', viewer: 'user-42' });
954
- // POST /api/products
955
- const createRes = await app.request('/api/products', {
956
- method: 'POST',
957
- headers: {
958
- 'Content-Type': 'application/json',
959
- 'X-User-Id': 'user-42',
960
- },
961
- body: JSON.stringify({ name: 'New Product', price: 29.99 }),
962
- });
963
- expect(createRes.status).toBe(201);
964
- expect(await createRes.json()).toEqual({
965
- id: 'prod-new',
966
- name: 'New Product',
967
- createdBy: 'user-42',
968
- });
969
- // DELETE /api/products/prod-1
970
- const deleteRes = await app.request('/api/products/prod-1', {
971
- method: 'DELETE',
972
- headers: { 'X-User-Id': 'user-42' },
973
- });
974
- expect(deleteRes.status).toBe(204);
975
- // Verify docs
976
- expect(builder.docs).toHaveLength(4);
977
- const paths = builder.docs.map((d) => `${d.method.toUpperCase()} ${d.fullPath}`);
978
- expect(paths).toContain('GET /api/products');
979
- expect(paths).toContain('GET /api/products/:id');
980
- expect(paths).toContain('POST /api/products');
981
- expect(paths).toContain('DELETE /api/products/:id');
982
- // Verify hooks
983
- expect(events).toContain('request-start');
984
- expect(events).toContain('success:ListProducts');
985
- expect(events).toContain('success:GetProduct');
986
- expect(events).toContain('success:CreateProduct');
987
- expect(events).toContain('success:DeleteProduct');
988
- expect(events).toContain('request-end');
989
- });
990
- });
991
- });
992
- //# sourceMappingURL=index.test.js.map