@c9up/ream 0.1.25 → 0.1.27

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 (302) hide show
  1. package/README.md +65 -0
  2. package/dist/ConfigLoader.d.ts +46 -5
  3. package/dist/ConfigLoader.d.ts.map +1 -1
  4. package/dist/ConfigLoader.js +60 -9
  5. package/dist/ConfigLoader.js.map +1 -1
  6. package/dist/HttpKernel.d.ts.map +1 -1
  7. package/dist/HttpKernel.js +46 -6
  8. package/dist/HttpKernel.js.map +1 -1
  9. package/dist/Ignitor.d.ts +115 -7
  10. package/dist/Ignitor.d.ts.map +1 -1
  11. package/dist/Ignitor.js +254 -40
  12. package/dist/Ignitor.js.map +1 -1
  13. package/dist/console/BaseCommand.d.ts +217 -0
  14. package/dist/console/BaseCommand.d.ts.map +1 -0
  15. package/dist/console/BaseCommand.js +340 -0
  16. package/dist/console/BaseCommand.js.map +1 -0
  17. package/dist/console/Console.d.ts +86 -0
  18. package/dist/console/Console.d.ts.map +1 -0
  19. package/dist/console/Console.js +134 -0
  20. package/dist/console/Console.js.map +1 -0
  21. package/dist/console/ExceptionHandler.d.ts +28 -0
  22. package/dist/console/ExceptionHandler.d.ts.map +1 -0
  23. package/dist/console/ExceptionHandler.js +76 -0
  24. package/dist/console/ExceptionHandler.js.map +1 -0
  25. package/dist/console/HelpCommand.d.ts +16 -0
  26. package/dist/console/HelpCommand.d.ts.map +1 -0
  27. package/dist/console/HelpCommand.js +40 -0
  28. package/dist/console/HelpCommand.js.map +1 -0
  29. package/dist/console/IndexGenerator.d.ts +24 -0
  30. package/dist/console/IndexGenerator.d.ts.map +1 -0
  31. package/dist/console/IndexGenerator.js +65 -0
  32. package/dist/console/IndexGenerator.js.map +1 -0
  33. package/dist/console/Kernel.d.ts +298 -0
  34. package/dist/console/Kernel.d.ts.map +1 -0
  35. package/dist/console/Kernel.js +1120 -0
  36. package/dist/console/Kernel.js.map +1 -0
  37. package/dist/console/ListCommand.d.ts +18 -0
  38. package/dist/console/ListCommand.d.ts.map +1 -0
  39. package/dist/console/ListCommand.js +60 -0
  40. package/dist/console/ListCommand.js.map +1 -0
  41. package/dist/console/assertions.d.ts +20 -0
  42. package/dist/console/assertions.d.ts.map +1 -0
  43. package/dist/console/assertions.js +84 -0
  44. package/dist/console/assertions.js.map +1 -0
  45. package/dist/console/cliui.d.ts +248 -0
  46. package/dist/console/cliui.d.ts.map +1 -0
  47. package/dist/console/cliui.js +587 -0
  48. package/dist/console/cliui.js.map +1 -0
  49. package/dist/console/decorators.d.ts +90 -0
  50. package/dist/console/decorators.d.ts.map +1 -0
  51. package/dist/console/decorators.js +142 -0
  52. package/dist/console/decorators.js.map +1 -0
  53. package/dist/console/index.d.ts +32 -0
  54. package/dist/console/index.d.ts.map +1 -0
  55. package/dist/console/index.js +24 -0
  56. package/dist/console/index.js.map +1 -0
  57. package/dist/console/loaders.d.ts +52 -0
  58. package/dist/console/loaders.d.ts.map +1 -0
  59. package/dist/console/loaders.js +131 -0
  60. package/dist/console/loaders.js.map +1 -0
  61. package/dist/console/parser.d.ts +86 -0
  62. package/dist/console/parser.d.ts.map +1 -0
  63. package/dist/console/parser.js +440 -0
  64. package/dist/console/parser.js.map +1 -0
  65. package/dist/console/prompts.d.ts +124 -0
  66. package/dist/console/prompts.d.ts.map +1 -0
  67. package/dist/console/prompts.js +356 -0
  68. package/dist/console/prompts.js.map +1 -0
  69. package/dist/console/tracing.d.ts +17 -0
  70. package/dist/console/tracing.d.ts.map +1 -0
  71. package/dist/console/tracing.js +11 -0
  72. package/dist/console/tracing.js.map +1 -0
  73. package/dist/console/types.d.ts +255 -0
  74. package/dist/console/types.d.ts.map +1 -0
  75. package/dist/console/types.js +25 -0
  76. package/dist/console/types.js.map +1 -0
  77. package/dist/console/ui.d.ts +20 -0
  78. package/dist/console/ui.d.ts.map +1 -0
  79. package/dist/console/ui.js +31 -0
  80. package/dist/console/ui.js.map +1 -0
  81. package/dist/console/utils.d.ts +27 -0
  82. package/dist/console/utils.d.ts.map +1 -0
  83. package/dist/console/utils.js +56 -0
  84. package/dist/console/utils.js.map +1 -0
  85. package/dist/container/Container.d.ts +23 -3
  86. package/dist/container/Container.d.ts.map +1 -1
  87. package/dist/container/Container.js +177 -70
  88. package/dist/container/Container.js.map +1 -1
  89. package/dist/container/types.d.ts +14 -2
  90. package/dist/container/types.d.ts.map +1 -1
  91. package/dist/decorators/Service.d.ts +7 -0
  92. package/dist/decorators/Service.d.ts.map +1 -1
  93. package/dist/decorators/Service.js +23 -3
  94. package/dist/decorators/Service.js.map +1 -1
  95. package/dist/env/Env.d.ts +29 -4
  96. package/dist/env/Env.d.ts.map +1 -1
  97. package/dist/env/Env.js +27 -4
  98. package/dist/env/Env.js.map +1 -1
  99. package/dist/env/Secret.d.ts +11 -0
  100. package/dist/env/Secret.d.ts.map +1 -0
  101. package/dist/env/Secret.js +36 -0
  102. package/dist/env/Secret.js.map +1 -0
  103. package/dist/env/interpolate.d.ts +26 -0
  104. package/dist/env/interpolate.d.ts.map +1 -0
  105. package/dist/env/interpolate.js +48 -0
  106. package/dist/env/interpolate.js.map +1 -0
  107. package/dist/env/loadEnvFiles.d.ts.map +1 -1
  108. package/dist/env/loadEnvFiles.js +14 -4
  109. package/dist/env/loadEnvFiles.js.map +1 -1
  110. package/dist/env/schema.d.ts +23 -6
  111. package/dist/env/schema.d.ts.map +1 -1
  112. package/dist/env/schema.js +67 -33
  113. package/dist/env/schema.js.map +1 -1
  114. package/dist/errors/ReamError.d.ts +25 -0
  115. package/dist/errors/ReamError.d.ts.map +1 -1
  116. package/dist/errors/ReamError.js +5 -0
  117. package/dist/errors/ReamError.js.map +1 -1
  118. package/dist/errors/prettyPrintError.d.ts +9 -0
  119. package/dist/errors/prettyPrintError.d.ts.map +1 -0
  120. package/dist/errors/prettyPrintError.js +22 -0
  121. package/dist/errors/prettyPrintError.js.map +1 -0
  122. package/dist/events/Emitter.d.ts +1 -1
  123. package/dist/events/Emitter.d.ts.map +1 -1
  124. package/dist/events/Emitter.js +1 -1
  125. package/dist/events/Emitter.js.map +1 -1
  126. package/dist/events/EventsProvider.d.ts +2 -2
  127. package/dist/events/EventsProvider.d.ts.map +1 -1
  128. package/dist/events/EventsProvider.js +3 -3
  129. package/dist/events/EventsProvider.js.map +1 -1
  130. package/dist/graphql/GraphQLEngine.d.ts +1 -1
  131. package/dist/graphql/GraphQLEngine.d.ts.map +1 -1
  132. package/dist/graphql/GraphQLEngine.js +2 -2
  133. package/dist/graphql/GraphQLEngine.js.map +1 -1
  134. package/dist/graphql/GraphQLProvider.d.ts.map +1 -1
  135. package/dist/graphql/GraphQLProvider.js +7 -3
  136. package/dist/graphql/GraphQLProvider.js.map +1 -1
  137. package/dist/http/Exception.d.ts +63 -1
  138. package/dist/http/Exception.d.ts.map +1 -1
  139. package/dist/http/Exception.js +185 -48
  140. package/dist/http/Exception.js.map +1 -1
  141. package/dist/http/HttpContext.d.ts +53 -7
  142. package/dist/http/HttpContext.d.ts.map +1 -1
  143. package/dist/http/HttpContext.js +56 -15
  144. package/dist/http/HttpContext.js.map +1 -1
  145. package/dist/http/RedirectBuilder.d.ts.map +1 -1
  146. package/dist/http/RedirectBuilder.js +4 -1
  147. package/dist/http/RedirectBuilder.js.map +1 -1
  148. package/dist/http/Request.d.ts +141 -10
  149. package/dist/http/Request.d.ts.map +1 -1
  150. package/dist/http/Request.js +244 -34
  151. package/dist/http/Request.js.map +1 -1
  152. package/dist/http/Response.d.ts +176 -9
  153. package/dist/http/Response.d.ts.map +1 -1
  154. package/dist/http/Response.js +404 -16
  155. package/dist/http/Response.js.map +1 -1
  156. package/dist/index.d.ts +19 -8
  157. package/dist/index.d.ts.map +1 -1
  158. package/dist/index.js +20 -5
  159. package/dist/index.js.map +1 -1
  160. package/dist/middleware/Pipeline.d.ts +29 -8
  161. package/dist/middleware/Pipeline.d.ts.map +1 -1
  162. package/dist/middleware/Pipeline.js +40 -2
  163. package/dist/middleware/Pipeline.js.map +1 -1
  164. package/dist/openapi/OpenApiProvider.js +4 -4
  165. package/dist/openapi/OpenApiProvider.js.map +1 -1
  166. package/dist/router/Router.d.ts +223 -31
  167. package/dist/router/Router.d.ts.map +1 -1
  168. package/dist/router/Router.js +520 -102
  169. package/dist/router/Router.js.map +1 -1
  170. package/dist/rpc/RpcProvider.d.ts +1 -0
  171. package/dist/rpc/RpcProvider.d.ts.map +1 -1
  172. package/dist/rpc/RpcProvider.js +7 -3
  173. package/dist/rpc/RpcProvider.js.map +1 -1
  174. package/dist/rpc/RpcRouter.d.ts +2 -2
  175. package/dist/rpc/RpcRouter.d.ts.map +1 -1
  176. package/dist/rpc/RpcRouter.js +17 -5
  177. package/dist/rpc/RpcRouter.js.map +1 -1
  178. package/dist/scheduler/ScheduleProvider.d.ts.map +1 -1
  179. package/dist/scheduler/ScheduleProvider.js +7 -15
  180. package/dist/scheduler/ScheduleProvider.js.map +1 -1
  181. package/dist/server/Server.d.ts +17 -4
  182. package/dist/server/Server.d.ts.map +1 -1
  183. package/dist/server/Server.js +41 -7
  184. package/dist/server/Server.js.map +1 -1
  185. package/dist/services/app.d.ts +1 -1
  186. package/dist/services/app.js +1 -1
  187. package/dist/services/console.d.ts +21 -0
  188. package/dist/services/console.d.ts.map +1 -0
  189. package/dist/services/console.js +35 -0
  190. package/dist/services/console.js.map +1 -0
  191. package/dist/session/SessionMiddleware.js +3 -3
  192. package/dist/session/SessionMiddleware.js.map +1 -1
  193. package/dist/storage/StaticProvider.js +1 -1
  194. package/dist/storage/StaticProvider.js.map +1 -1
  195. package/dist/testing/ApiResponse.d.ts +218 -0
  196. package/dist/testing/ApiResponse.d.ts.map +1 -0
  197. package/dist/testing/ApiResponse.js +576 -0
  198. package/dist/testing/ApiResponse.js.map +1 -0
  199. package/dist/testing/RequestBuilder.d.ts +155 -35
  200. package/dist/testing/RequestBuilder.d.ts.map +1 -1
  201. package/dist/testing/RequestBuilder.js +455 -109
  202. package/dist/testing/RequestBuilder.js.map +1 -1
  203. package/dist/testing/TestClient.d.ts +53 -37
  204. package/dist/testing/TestClient.d.ts.map +1 -1
  205. package/dist/testing/TestClient.js +84 -52
  206. package/dist/testing/TestClient.js.map +1 -1
  207. package/dist/utils/Macroable.d.ts +30 -0
  208. package/dist/utils/Macroable.d.ts.map +1 -0
  209. package/dist/utils/Macroable.js +43 -0
  210. package/dist/utils/Macroable.js.map +1 -0
  211. package/dist/utils/inflect.d.ts +17 -0
  212. package/dist/utils/inflect.d.ts.map +1 -0
  213. package/dist/utils/inflect.js +32 -0
  214. package/dist/utils/inflect.js.map +1 -0
  215. package/dist/utils/objectPath.d.ts +57 -0
  216. package/dist/utils/objectPath.d.ts.map +1 -0
  217. package/dist/utils/objectPath.js +141 -0
  218. package/dist/utils/objectPath.js.map +1 -0
  219. package/events.darwin-arm64.node +0 -0
  220. package/events.darwin-x64.node +0 -0
  221. package/events.linux-arm64-gnu.node +0 -0
  222. package/events.linux-x64-gnu.node +0 -0
  223. package/events.win32-x64-msvc.node +0 -0
  224. package/index.darwin-arm64.node +0 -0
  225. package/index.darwin-x64.node +0 -0
  226. package/index.linux-arm64-gnu.node +0 -0
  227. package/index.linux-x64-gnu.node +0 -0
  228. package/index.win32-x64-msvc.node +0 -0
  229. package/package.json +20 -2
  230. package/scheduler.darwin-arm64.node +0 -0
  231. package/scheduler.darwin-x64.node +0 -0
  232. package/scheduler.linux-arm64-gnu.node +0 -0
  233. package/scheduler.linux-x64-gnu.node +0 -0
  234. package/scheduler.win32-x64-msvc.node +0 -0
  235. package/src/ConfigLoader.ts +72 -9
  236. package/src/HttpKernel.ts +53 -15
  237. package/src/Ignitor.ts +364 -45
  238. package/src/console/BaseCommand.ts +467 -0
  239. package/src/console/Console.ts +174 -0
  240. package/src/console/ExceptionHandler.ts +91 -0
  241. package/src/console/HelpCommand.ts +37 -0
  242. package/src/console/IndexGenerator.ts +79 -0
  243. package/src/console/Kernel.ts +1382 -0
  244. package/src/console/ListCommand.ts +55 -0
  245. package/src/console/assertions.ts +114 -0
  246. package/src/console/cliui.ts +756 -0
  247. package/src/console/decorators.ts +199 -0
  248. package/src/console/index.ts +67 -0
  249. package/src/console/loaders.ts +145 -0
  250. package/src/console/parser.ts +598 -0
  251. package/src/console/prompts.ts +548 -0
  252. package/src/console/tracing.ts +22 -0
  253. package/src/console/types.ts +280 -0
  254. package/src/console/ui.ts +37 -0
  255. package/src/console/utils.ts +69 -0
  256. package/src/container/Container.ts +198 -72
  257. package/src/container/types.ts +15 -2
  258. package/src/decorators/Service.ts +28 -3
  259. package/src/env/Env.ts +49 -5
  260. package/src/env/Secret.ts +41 -0
  261. package/src/env/interpolate.ts +54 -0
  262. package/src/env/loadEnvFiles.ts +12 -4
  263. package/src/env/schema.ts +109 -35
  264. package/src/errors/ReamError.ts +31 -0
  265. package/src/errors/prettyPrintError.ts +20 -0
  266. package/src/events/Emitter.ts +2 -2
  267. package/src/events/EventsProvider.ts +5 -5
  268. package/src/graphql/GraphQLEngine.ts +4 -4
  269. package/src/graphql/GraphQLProvider.ts +7 -3
  270. package/src/http/Exception.ts +214 -37
  271. package/src/http/HttpContext.ts +87 -18
  272. package/src/http/RedirectBuilder.ts +4 -1
  273. package/src/http/Request.ts +264 -34
  274. package/src/http/Response.ts +478 -27
  275. package/src/index.ts +41 -6
  276. package/src/middleware/Pipeline.ts +77 -6
  277. package/src/openapi/OpenApiProvider.ts +4 -4
  278. package/src/router/Router.ts +631 -113
  279. package/src/rpc/RpcProvider.ts +6 -2
  280. package/src/rpc/RpcRouter.ts +22 -7
  281. package/src/scheduler/ScheduleProvider.ts +7 -15
  282. package/src/server/Server.ts +49 -10
  283. package/src/services/app.ts +1 -1
  284. package/src/services/console.ts +42 -0
  285. package/src/session/SessionMiddleware.ts +4 -4
  286. package/src/storage/StaticProvider.ts +1 -1
  287. package/src/testing/ApiResponse.ts +691 -0
  288. package/src/testing/RequestBuilder.ts +577 -137
  289. package/src/testing/TestClient.ts +110 -77
  290. package/src/utils/Macroable.ts +54 -0
  291. package/src/utils/inflect.ts +29 -0
  292. package/src/utils/objectPath.ts +142 -0
  293. package/dist/console/CommandRunner.d.ts +0 -18
  294. package/dist/console/CommandRunner.d.ts.map +0 -1
  295. package/dist/console/CommandRunner.js +0 -65
  296. package/dist/console/CommandRunner.js.map +0 -1
  297. package/dist/decorators/Lazy.d.ts +0 -19
  298. package/dist/decorators/Lazy.d.ts.map +0 -1
  299. package/dist/decorators/Lazy.js +0 -53
  300. package/dist/decorators/Lazy.js.map +0 -1
  301. package/src/console/CommandRunner.ts +0 -82
  302. package/src/decorators/Lazy.ts +0 -57
@@ -1,10 +1,51 @@
1
- /** Assertion failure — plain Error so tests see a clean stack trace. */
2
- class ExpectationError extends Error {
3
- constructor(message) {
4
- super(message);
5
- this.name = 'ExpectationError';
6
- }
1
+ /**
2
+ * Fluent HTTP request builder with assertion methods (`@japa/api-client` model).
3
+ *
4
+ * await client
5
+ * .get('/api/users/42')
6
+ * .withAuth(user)
7
+ * .assertOk()
8
+ * .assertBody({ id: 42 })
9
+ *
10
+ * The builder chains SYNCHRONOUSLY: setters and assertion methods both return
11
+ * `this`. Assertions are LAZY — each registers a check; the request is sent once
12
+ * and every check runs, in order, when the builder is awaited (it is thenable,
13
+ * so `await builder` resolves to the {@link ApiResponse}). A plain
14
+ * `await client.get('/x')` (no assertion) just sends and returns the response.
15
+ */
16
+ import { readFileSync } from 'node:fs';
17
+ import { basename } from 'node:path';
18
+ import { ApiResponse, capBody, ExpectationError, partialMatch, STATUS_ASSERTIONS, } from './ApiResponse.js';
19
+ // Re-export the shared response types + matchers so `@c9up/ream/testing`
20
+ // consumers keep importing them from here (back-compat).
21
+ export { ApiResponse, deepEqual, partialMatch, } from './ApiResponse.js';
22
+ /** Cookie the signed CSRF token is issued in (blackhole default: `XSRF-TOKEN`). */
23
+ const CSRF_COOKIE_NAME = 'XSRF-TOKEN';
24
+ /** Header the client echoes the token back in (Axios/Angular default). */
25
+ const CSRF_HEADER_NAME = 'x-xsrf-token';
26
+ /** Content-type / accept shorthands — mirrors japa/api-client's `.type()`/`.accept()`. */
27
+ const TYPE_SHORTHANDS = {
28
+ json: 'application/json',
29
+ form: 'application/x-www-form-urlencoded',
30
+ urlencoded: 'application/x-www-form-urlencoded',
31
+ text: 'text/plain',
32
+ html: 'text/html',
33
+ xml: 'application/xml',
34
+ };
35
+ /** Expand a shorthand (`'json'`) to a full MIME type, or pass a full type through. */
36
+ function resolveMime(type) {
37
+ return TYPE_SHORTHANDS[type] ?? type;
7
38
  }
39
+ /** HTTP statuses that trigger redirect-following (Japa `.redirects()`). */
40
+ const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
41
+ /** Japa follows 5 redirects by default. */
42
+ const DEFAULT_MAX_REDIRECTS = 5;
43
+ /** Consumer-registered macros/getters (Japa `ApiRequest.macro`/`.getter`). */
44
+ const requestMacros = new Map();
45
+ const requestGetters = new Map();
46
+ /** Consumer-registered request body serializers (Japa `ApiRequest.addSerializer`), by name. */
47
+ const requestSerializers = new Map();
48
+ // biome-ignore lint/suspicious/noUnsafeDeclarationMerging: the merged interface (StatusAssertions) types the status-shortcut asserts attached from STATUS_ASSERTIONS at load — the same generated-method pattern as EonSchema/Macroable (AdonisJS parity); every member is implemented, so the merge is safe.
8
49
  export class RequestBuilder {
9
50
  #sender;
10
51
  #method;
@@ -13,14 +54,22 @@ export class RequestBuilder {
13
54
  #body = Buffer.alloc(0);
14
55
  #multipart = [];
15
56
  #cookies = {};
57
+ #query = new URLSearchParams();
16
58
  #authStrategy;
17
59
  #pendingAuth = null;
60
+ #timeoutMs;
61
+ #maxRedirects = DEFAULT_MAX_REDIRECTS;
18
62
  #sent = null;
63
+ // Lazy assertions (japa model): each `assert*`/`expect*` registers a check and
64
+ // returns `this` synchronously; the checks run in order when the builder is
65
+ // awaited (`then`) — after the single send.
66
+ #checks = [];
19
67
  constructor(sender, method, path, authStrategy = null) {
20
68
  this.#sender = sender;
21
69
  this.#method = method;
22
70
  this.#path = path;
23
71
  this.#authStrategy = authStrategy;
72
+ applyRequestExtensions(this);
24
73
  }
25
74
  headers(map) {
26
75
  for (const [k, v] of Object.entries(map)) {
@@ -29,7 +78,8 @@ export class RequestBuilder {
29
78
  return this;
30
79
  }
31
80
  header(name, value) {
32
- this.#headers[name.toLowerCase()] = value;
81
+ // Japa accepts string | string[]; a list is joined into one header value.
82
+ this.#headers[name.toLowerCase()] = Array.isArray(value) ? value.join(', ') : value;
33
83
  return this;
34
84
  }
35
85
  json(data) {
@@ -37,6 +87,10 @@ export class RequestBuilder {
37
87
  this.#headers['content-type'] = 'application/json';
38
88
  return this;
39
89
  }
90
+ /** Japa `.unsafeJson()` — set a JSON body without any transform (alias of {@link json}). */
91
+ unsafeJson(data) {
92
+ return this.json(data);
93
+ }
40
94
  body(data, contentType) {
41
95
  this.#body = typeof data === 'string' ? Buffer.from(data, 'utf8') : data;
42
96
  if (contentType)
@@ -44,34 +98,64 @@ export class RequestBuilder {
44
98
  return this;
45
99
  }
46
100
  form(data) {
47
- const pairs = Object.entries(data).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`);
101
+ // Array values repeat the key (`a=1&a=2`) — Japa parity.
102
+ const pairs = [];
103
+ for (const [k, v] of Object.entries(data)) {
104
+ const values = Array.isArray(v) ? v : [v];
105
+ for (const item of values) {
106
+ pairs.push(`${encodeURIComponent(k)}=${encodeURIComponent(fieldToString(item))}`);
107
+ }
108
+ }
48
109
  this.#body = Buffer.from(pairs.join('&'), 'utf8');
49
110
  this.#headers['content-type'] = 'application/x-www-form-urlencoded';
50
111
  return this;
51
112
  }
113
+ /** Japa `.unsafeForm()` — set a urlencoded body without transform (alias of {@link form}). */
114
+ unsafeForm(data) {
115
+ return this.form(data);
116
+ }
52
117
  /**
53
- * Add a `multipart/form-data` text field — mirrors Adonis/japa's `.field()`.
54
- * Calling `.field()` or `.file()` switches the body to multipart (use
55
- * `.form()` for `application/x-www-form-urlencoded`). The boundary +
56
- * `Content-Type` header are produced at send time.
118
+ * Add a `multipart/form-data` field — mirrors Adonis/japa's `.field()`. Accepts
119
+ * a string/number/boolean, a Buffer, a Blob, a Readable stream, or an array of
120
+ * these (repeated part). Calling `.field()`/`.fields()`/`.file()` switches the
121
+ * body to multipart; the boundary + `Content-Type` header are produced at send
122
+ * time (streams/blobs are drained to Buffers then).
57
123
  */
58
124
  field(name, value) {
59
- this.#multipart.push({ kind: 'field', name, value });
125
+ const values = Array.isArray(value) ? value : [value];
126
+ for (const v of values) {
127
+ this.#multipart.push({
128
+ kind: 'field',
129
+ name,
130
+ value: isScalar(v) ? fieldToString(v) : v,
131
+ });
132
+ }
133
+ return this;
134
+ }
135
+ /** Add several multipart fields at once — Japa `.fields()`. */
136
+ fields(map) {
137
+ for (const [name, value] of Object.entries(map)) {
138
+ this.field(name, value);
139
+ }
60
140
  return this;
61
141
  }
62
142
  /**
63
143
  * Attach a file as a `multipart/form-data` part — mirrors Adonis/japa's
64
- * `.file(field, contents, { filename, contentType })`. `contents` is a
65
- * Buffer (binary) or a string (encoded as utf8). Server-side
66
- * `request.file(field, …)` reads the resulting part.
144
+ * `.file(field, value, { filename, contentType })`. `value` is a Buffer, a
145
+ * Blob, a Readable stream, OR a string absolute PATH to a file on disk (read at
146
+ * call time, Japa parity). The filename defaults to the path basename / field
147
+ * name; streams/blobs are drained to Buffers at send time.
67
148
  */
68
- file(field, contents, options = {}) {
149
+ file(field, value, options = {}) {
150
+ // A string is a PATH → read now; other kinds are resolved at send time.
151
+ const content = typeof value === 'string' ? readFileSync(value) : value;
152
+ const filename = options.filename ?? (typeof value === 'string' ? basename(value) : field);
69
153
  this.#multipart.push({
70
154
  kind: 'file',
71
155
  name: field,
72
- filename: options.filename ?? field,
156
+ filename,
73
157
  contentType: options.contentType ?? 'application/octet-stream',
74
- content: typeof contents === 'string' ? Buffer.from(contents, 'utf8') : contents,
158
+ content,
75
159
  });
76
160
  return this;
77
161
  }
@@ -83,10 +167,120 @@ export class RequestBuilder {
83
167
  this.#cookies[name] = value;
84
168
  return this;
85
169
  }
170
+ /**
171
+ * Append query-string params — mirrors japa/api-client's `.qs()`. Values are
172
+ * URL-encoded; arrays repeat the key (`?tag=a&tag=b`). Merges with any query
173
+ * string already on the path and with earlier `.qs()` calls.
174
+ */
175
+ qs(params) {
176
+ for (const [key, value] of Object.entries(params)) {
177
+ if (Array.isArray(value)) {
178
+ for (const item of value)
179
+ this.#query.append(key, String(item));
180
+ }
181
+ else {
182
+ this.#query.append(key, String(value));
183
+ }
184
+ }
185
+ return this;
186
+ }
187
+ /**
188
+ * Append query params WITHOUT any validation/normalisation — Japa
189
+ * `.unsafeQs({ … })`. Values are still URL-encoded so the wire stays valid, but
190
+ * (unlike `.qs()`) no key filtering is applied; arrays repeat the key.
191
+ */
192
+ unsafeQs(params) {
193
+ return this.qs(params);
194
+ }
195
+ /** Pass a bearer token as the `Authorization` header — japa `.bearerToken()`. */
196
+ bearerToken(token) {
197
+ this.#headers.authorization = `Bearer ${token}`;
198
+ return this;
199
+ }
200
+ /** Pass HTTP Basic credentials as the `Authorization` header — japa `.basicAuth()`. */
201
+ basicAuth(user, password) {
202
+ const encoded = Buffer.from(`${user}:${password}`, 'utf8').toString('base64');
203
+ this.#headers.authorization = `Basic ${encoded}`;
204
+ return this;
205
+ }
206
+ /**
207
+ * Set the request `Content-Type` from a shorthand (`'json'`, `'form'`, …) or a
208
+ * full MIME type — mirrors japa/api-client's `.type()`.
209
+ */
210
+ type(mime) {
211
+ this.#headers['content-type'] = resolveMime(mime);
212
+ return this;
213
+ }
214
+ /**
215
+ * Set the `Accept` header from a shorthand (`'json'`, `'html'`, …) or a full
216
+ * MIME type — mirrors japa/api-client's `.accept()`.
217
+ */
218
+ accept(mime) {
219
+ this.#headers.accept = resolveMime(mime);
220
+ return this;
221
+ }
222
+ /** Per-request timeout in ms — Japa `.timeout()`. Honoured by the sender. */
223
+ timeout(ms) {
224
+ this.#timeoutMs = ms;
225
+ return this;
226
+ }
227
+ /**
228
+ * Follow up to `count` redirects before resolving — Japa `.redirects()`.
229
+ * Defaults to 5 (Japa default); pass `0` to disable following.
230
+ */
231
+ redirects(count) {
232
+ this.#maxRedirects = Math.max(0, count);
233
+ return this;
234
+ }
235
+ // TLS knobs — Japa parity. The test server is plain HTTP over loopback, so
236
+ // these are inert (there is no TLS handshake to configure); they exist so
237
+ // Japa-shaped code type-checks and runs unchanged. NAMED as a ream deviation.
238
+ /** No-op: the loopback test server has no TLS — Japa `.trustLocalhost()`. */
239
+ trustLocalhost(_trust = true) {
240
+ return this;
241
+ }
242
+ /** No-op: no TLS on the loopback test server — Japa `.ca()`. */
243
+ ca(_cert) {
244
+ return this;
245
+ }
246
+ /** No-op: no TLS on the loopback test server — Japa `.cert()`. */
247
+ cert(_chain) {
248
+ return this;
249
+ }
250
+ /** No-op: no TLS on the loopback test server — Japa `.privateKey()`. */
251
+ privateKey(_key) {
252
+ return this;
253
+ }
254
+ /** No-op: no TLS on the loopback test server — Japa `.pfx()`. */
255
+ pfx(_encoded) {
256
+ return this;
257
+ }
258
+ /** No-op: no TLS on the loopback test server — Japa `.disableTLSCerts()`. */
259
+ disableTLSCerts() {
260
+ return this;
261
+ }
262
+ /**
263
+ * Satisfy blackhole's signed double-submit CSRF check: echo the `XSRF-TOKEN`
264
+ * cookie back in the `X-XSRF-TOKEN` header (the pair the server compares).
265
+ */
266
+ withCsrf(token) {
267
+ if (token !== undefined)
268
+ this.#cookies[CSRF_COOKIE_NAME] = token;
269
+ const value = this.#cookies[CSRF_COOKIE_NAME];
270
+ if (value === undefined) {
271
+ throw new Error(`RequestBuilder: withCsrf() found no '${CSRF_COOKIE_NAME}' cookie. Set it first via ` +
272
+ `.cookie('${CSRF_COOKIE_NAME}', token) (extracted from a prior safe GET) or pass the token: withCsrf(token).`);
273
+ }
274
+ this.#headers[CSRF_HEADER_NAME] = value;
275
+ return this;
276
+ }
277
+ /** japa/api-client's spelling of {@link withCsrf}. */
278
+ withCsrfToken(token) {
279
+ return this.withCsrf(token);
280
+ }
86
281
  /**
87
282
  * Attach auth for a user. Uses the strategy passed to `client`'s `auth`
88
- * option — Warden's session/JWT/API-key strategy decides the header/cookie
89
- * shape. Without a strategy, throws at send time.
283
+ * option. Without a strategy, throws at send time.
90
284
  */
91
285
  withAuth(subject) {
92
286
  this.#pendingAuth = subject;
@@ -97,71 +291,105 @@ export class RequestBuilder {
97
291
  this.#pendingAuth = { id: userId };
98
292
  return this;
99
293
  }
100
- /** Fire the request (once) and return the raw response. Idempotent. */
294
+ /** Fire the request (once) and return the rich response. Idempotent. */
101
295
  send() {
102
296
  if (this.#sent === null)
103
297
  this.#sent = this.#execute();
104
298
  return this.#sent;
105
299
  }
106
- async expectStatus(code) {
300
+ /**
301
+ * Thenable — `await client.get('/x')` sends the request and resolves to the
302
+ * {@link ApiResponse} (Japa/api-client parity). Assertion methods return `this`;
303
+ * awaiting one resolves via this `then` to the response after running the checks.
304
+ */
305
+ // biome-ignore lint/suspicious/noThenProperty: the thenable IS the public API — `await client.get('/x')` is the documented Japa/api-client ergonomic; removing `.then` breaks every await-the-builder call site.
306
+ then(onFulfilled, onRejected) {
307
+ return this.#settle().then(onFulfilled, onRejected);
308
+ }
309
+ /** Send once, then run every registered assertion (throws on first failure). */
310
+ async #settle() {
107
311
  const res = await this.send();
108
- if (res.status !== code) {
109
- throw new ExpectationError(`Expected status ${code}, got ${res.status}. Body: ${capBody(res.body)}`);
110
- }
312
+ for (const check of this.#checks)
313
+ check(res);
314
+ return res;
315
+ }
316
+ /** Register a lazy assertion; returns `this` for synchronous chaining. */
317
+ #assert(check) {
318
+ this.#checks.push(check);
111
319
  return this;
112
320
  }
113
- async expectHeader(name, value) {
114
- const res = await this.send();
115
- const actual = res.headers[name.toLowerCase()];
116
- if (actual === undefined) {
117
- throw new ExpectationError(`Expected header ${name}, not present in response headers: ${Object.keys(res.headers).join(', ')}`);
118
- }
119
- if (value instanceof RegExp) {
120
- if (!value.test(actual)) {
121
- throw new ExpectationError(`Expected header ${name} to match ${value}, got "${actual}"`);
321
+ // ── ream-flavoured `expect*` aliases (kept for back-compat) ───────────────
322
+ expectStatus(code) {
323
+ return this.#assert((res) => res.assertStatus(code));
324
+ }
325
+ expectHeader(name, value) {
326
+ return this.#assert((res) => res.assertHeader(name, value));
327
+ }
328
+ expectCookie(name, value) {
329
+ return this.#assert((res) => res.assertCookie(name, value));
330
+ }
331
+ expectJson(expected) {
332
+ // ream alias: partial JSON match with its own message (kept for back-compat;
333
+ // `assertBodyContains` is the Japa-named equivalent).
334
+ return this.#assert((res) => {
335
+ if (!partialMatch(res.json(), expected)) {
336
+ throw new ExpectationError(`JSON partial match failed.\nExpected (partial): ${JSON.stringify(expected)}\nActual: ${capBody(JSON.stringify(res.json()))}`);
122
337
  }
123
- }
124
- else if (actual !== value) {
125
- throw new ExpectationError(`Expected header ${name} = "${value}", got "${actual}"`);
126
- }
338
+ });
339
+ }
340
+ // ── Japa assertions (lazy — delegate to the response) ─────────────────────
341
+ assertStatus(code) {
342
+ return this.#assert((res) => res.assertStatus(code));
343
+ }
344
+ assertHeader(name, value) {
345
+ return this.#assert((res) => res.assertHeader(name, value));
346
+ }
347
+ assertHeaderMissing(name) {
348
+ return this.#assert((res) => res.assertHeaderMissing(name));
349
+ }
350
+ assertCookie(name, value) {
351
+ return this.#assert((res) => res.assertCookie(name, value));
352
+ }
353
+ assertCookieMissing(name) {
354
+ return this.#assert((res) => res.assertCookieMissing(name));
355
+ }
356
+ assertBody(expected) {
357
+ return this.#assert((res) => res.assertBody(expected));
358
+ }
359
+ assertBodyContains(subset) {
360
+ return this.#assert((res) => res.assertBodyContains(subset));
361
+ }
362
+ assertBodyNotContains(subset) {
363
+ return this.#assert((res) => res.assertBodyNotContains(subset));
364
+ }
365
+ assertTextIncludes(substring) {
366
+ return this.#assert((res) => res.assertTextIncludes(substring));
367
+ }
368
+ assertRedirectsTo(path) {
369
+ return this.#assert((res) => res.assertRedirectsTo(path));
370
+ }
371
+ // ── Request debugging (Japa `request.dump*` — dumps the REQUEST) ──────────
372
+ /** Print the pending request (method/path/headers/body) — Japa `request.dump()`. */
373
+ dump() {
374
+ process.stderr.write(`[helix:request.dump] ${this.#method} ${this.#path}\n` +
375
+ `headers: ${JSON.stringify(this.#headers, null, 2)}\n` +
376
+ `cookies: ${JSON.stringify(this.#cookies, null, 2)}\n` +
377
+ `body: ${capBody(this.#body.toString('utf8'), 2048)}\n`);
127
378
  return this;
128
379
  }
129
- async expectCookie(name, value) {
130
- const res = await this.send();
131
- const setCookie = res.headers['set-cookie'];
132
- if (!setCookie) {
133
- throw new ExpectationError(`Expected cookie ${name}, but no Set-Cookie header was returned`);
134
- }
135
- const cookies = setCookie.split(/,(?=\s*\w+=)/);
136
- const match = cookies.find((c) => c.trimStart().startsWith(`${name}=`));
137
- if (!match) {
138
- throw new ExpectationError(`Expected cookie ${name}, not found in: ${setCookie}`);
139
- }
140
- if (value === undefined)
141
- return this;
142
- const rawVal = match.split(';')[0]?.split('=')[1] ?? '';
143
- if (value instanceof RegExp) {
144
- if (!value.test(rawVal)) {
145
- throw new ExpectationError(`Expected cookie ${name} to match ${value}, got "${rawVal}"`);
146
- }
147
- }
148
- else if (rawVal !== value) {
149
- throw new ExpectationError(`Expected cookie ${name} = "${value}", got "${rawVal}"`);
150
- }
380
+ /** Print only the pending request body — Japa `request.dumpBody()`. */
381
+ dumpBody() {
382
+ process.stderr.write(`[helix:request.dumpBody] ${capBody(this.#body.toString('utf8'), 2048)}\n`);
151
383
  return this;
152
384
  }
153
- async expectJson(expected) {
154
- const res = await this.send();
155
- let actual;
156
- try {
157
- actual = res.json();
158
- }
159
- catch (err) {
160
- throw new ExpectationError(`Expected JSON body, got non-JSON. Body: ${capBody(res.body)} (parse error: ${err instanceof Error ? err.message : String(err)})`);
161
- }
162
- if (!partialMatch(actual, expected)) {
163
- throw new ExpectationError(`JSON partial match failed.\nExpected (partial): ${JSON.stringify(expected)}\nActual: ${capBody(JSON.stringify(actual))}`);
164
- }
385
+ /** Print only the pending request headers — Japa `request.dumpHeaders()`. */
386
+ dumpHeaders() {
387
+ process.stderr.write(`[helix:request.dumpHeaders] ${JSON.stringify(this.#headers, null, 2)}\n`);
388
+ return this;
389
+ }
390
+ /** Print only the pending request cookies — Japa `request.dumpCookies()`. */
391
+ dumpCookies() {
392
+ process.stderr.write(`[helix:request.dumpCookies] ${JSON.stringify(this.#cookies, null, 2)}\n`);
165
393
  return this;
166
394
  }
167
395
  async #execute() {
@@ -188,7 +416,17 @@ export class RequestBuilder {
188
416
  // json()/form()/body(); encode them with a fresh boundary at send time.
189
417
  if (this.#multipart.length > 0) {
190
418
  const boundary = `----ReamRequestBuilder${crypto.randomUUID().replace(/-/g, '')}`;
191
- this.#body = encodeMultipart(this.#multipart, boundary);
419
+ // Drain any stream/blob part to a Buffer before encoding (Japa parity).
420
+ const resolved = await Promise.all(this.#multipart.map(async (p) => p.kind === 'field'
421
+ ? { kind: 'field', name: p.name, value: await toBuffer(p.value) }
422
+ : {
423
+ kind: 'file',
424
+ name: p.name,
425
+ filename: p.filename,
426
+ contentType: p.contentType,
427
+ content: await toBuffer(p.content),
428
+ }));
429
+ this.#body = encodeMultipart(resolved, boundary);
192
430
  this.#headers['content-type'] = `multipart/form-data; boundary=${boundary}`;
193
431
  }
194
432
  // Serialise cookies into a single `Cookie:` header.
@@ -196,28 +434,164 @@ export class RequestBuilder {
196
434
  if (cookieEntries.length > 0) {
197
435
  this.#headers.cookie = cookieEntries.map(([k, v]) => `${k}=${v}`).join('; ');
198
436
  }
199
- return this.#sender(this.#method, this.#path, {
437
+ // Merge `.qs()` params into the path, preserving any existing query string.
438
+ let path = this.#path;
439
+ const qs = this.#query.toString();
440
+ if (qs) {
441
+ path += (path.includes('?') ? '&' : '?') + qs;
442
+ }
443
+ let method = this.#method;
444
+ let body = this.#body;
445
+ let raw = await this.#sender(method, path, {
200
446
  headers: this.#headers,
201
- body: this.#body,
447
+ body,
448
+ timeoutMs: this.#timeoutMs,
202
449
  });
450
+ // Follow redirects up to the configured budget — Japa `.redirects()` (5 by default).
451
+ const chain = [];
452
+ let followed = 0;
453
+ while (followed < this.#maxRedirects &&
454
+ REDIRECT_STATUSES.has(raw.status) &&
455
+ raw.headers.location !== undefined) {
456
+ const location = raw.headers.location;
457
+ chain.push(location);
458
+ followed += 1;
459
+ path = location.startsWith('http')
460
+ ? new URL(location).pathname + new URL(location).search
461
+ : location;
462
+ // 303 (and, per browser convention, 301/302) switch to GET with no body;
463
+ // 307/308 preserve the method and body.
464
+ if (raw.status === 307 || raw.status === 308) {
465
+ // keep method + body
466
+ }
467
+ else {
468
+ method = 'GET';
469
+ body = Buffer.alloc(0);
470
+ delete this.#headers['content-type'];
471
+ }
472
+ raw = await this.#sender(method, path, {
473
+ headers: this.#headers,
474
+ body,
475
+ timeoutMs: this.#timeoutMs,
476
+ });
477
+ }
478
+ return new ApiResponse(raw, { method, redirects: chain });
479
+ }
480
+ /**
481
+ * Serialize `data` with a registered serializer and set it as the request body
482
+ * — the send-side twin of {@link ApiResponse.addParser}. Register serializers
483
+ * via {@link RequestBuilder.addSerializer} (Japa `ApiRequest.addSerializer`).
484
+ */
485
+ serialize(name, data) {
486
+ const serializer = requestSerializers.get(name);
487
+ if (!serializer) {
488
+ throw new Error(`RequestBuilder: no serializer registered for "${name}" (see RequestBuilder.addSerializer).`);
489
+ }
490
+ const { body, contentType } = serializer(data);
491
+ this.#body = body;
492
+ this.#headers['content-type'] = contentType;
493
+ return this;
494
+ }
495
+ /**
496
+ * Register a shared property (Japa `ApiRequest.macro`) or lazy getter
497
+ * (`ApiRequest.getter`). The callback is invoked with `this` bound to the
498
+ * builder AND the builder as its first arg, so both Japa's
499
+ * `function () { return this.header('x') }` and `(req) => …` work.
500
+ */
501
+ static macro(name, value) {
502
+ requestMacros.set(name, value);
203
503
  }
504
+ static getter(name, fn) {
505
+ requestGetters.set(name, fn);
506
+ }
507
+ /** Register a request body serializer — Japa `ApiRequest.addSerializer`. */
508
+ static addSerializer(name, fn) {
509
+ requestSerializers.set(name, fn);
510
+ }
511
+ /** Register a response body parser — Japa `ApiRequest.addParser` (delegates to ApiResponse). */
512
+ static addParser(contentType, fn) {
513
+ ApiResponse.addParser(contentType, fn);
514
+ }
515
+ }
516
+ // Attach the lazy status-shortcut assertions from the shared map so the builder
517
+ // and ApiResponse never disagree on the code set. Each defers to the public
518
+ // `expectStatus` (private fields aren't reachable from prototype-added fns).
519
+ for (const [method, code] of Object.entries(STATUS_ASSERTIONS)) {
520
+ Object.defineProperty(RequestBuilder.prototype, method, {
521
+ value() {
522
+ return this.expectStatus(code);
523
+ },
524
+ enumerable: false,
525
+ configurable: true,
526
+ writable: true,
527
+ });
204
528
  }
205
- function capBody(s, max = 512) {
206
- return s.length <= max ? s : `${s.slice(0, max)}...[truncated]`;
529
+ /** Coerce a field value to its wire string (Buffers are handled separately). */
530
+ function fieldToString(value) {
531
+ return Buffer.isBuffer(value) ? value.toString('utf8') : String(value);
532
+ }
533
+ /** Whether a multipart value is a scalar (string/number/boolean/Buffer), not a stream/blob. */
534
+ function isScalar(v) {
535
+ return (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean' || Buffer.isBuffer(v));
536
+ }
537
+ /** Drain a multipart value (string/Buffer/Blob/Readable) to a Buffer at send time. */
538
+ async function toBuffer(v) {
539
+ if (Buffer.isBuffer(v))
540
+ return v;
541
+ if (typeof v === 'string')
542
+ return Buffer.from(v, 'utf8');
543
+ if (typeof v === 'number' || typeof v === 'boolean')
544
+ return Buffer.from(String(v), 'utf8');
545
+ // `in` narrows the Blob | Readable union without a cast: only Blob has arrayBuffer.
546
+ if ('arrayBuffer' in v)
547
+ return Buffer.from(await v.arrayBuffer());
548
+ // Readable / async-iterable → collect chunks.
549
+ const chunks = [];
550
+ for await (const chunk of v) {
551
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
552
+ }
553
+ return Buffer.concat(chunks);
554
+ }
555
+ /** Apply registered request macros/getters onto a freshly built builder. */
556
+ function applyRequestExtensions(req) {
557
+ for (const [name, value] of requestMacros) {
558
+ Object.defineProperty(req, name, {
559
+ value,
560
+ enumerable: true,
561
+ configurable: true,
562
+ writable: true,
563
+ });
564
+ }
565
+ for (const [name, fn] of requestGetters) {
566
+ let computed = false;
567
+ let cached;
568
+ Object.defineProperty(req, name, {
569
+ enumerable: true,
570
+ configurable: true,
571
+ get() {
572
+ if (!computed) {
573
+ // `this`-bound AND passed as arg → both `function(){this.x}` and `(req)=>req.x` work.
574
+ cached = fn.call(req, req);
575
+ computed = true;
576
+ }
577
+ return cached;
578
+ },
579
+ });
580
+ }
207
581
  }
208
582
  /** Strip CR/LF and escape quotes so a name/filename can't break the headers. */
209
583
  function sanitizeHeaderParam(value) {
210
584
  return value.replace(/[\r\n]/g, '').replace(/"/g, '%22');
211
585
  }
212
- /** Encode multipart/form-data parts (RFC 7578) into a single body Buffer. */
586
+ /** Encode resolved multipart/form-data parts (RFC 7578) into a single body Buffer. */
213
587
  function encodeMultipart(parts, boundary) {
214
588
  const CRLF = '\r\n';
215
589
  const chunks = [];
216
590
  for (const part of parts) {
217
591
  if (part.kind === 'field') {
592
+ const valueBuf = part.value;
218
593
  chunks.push(Buffer.from(`--${boundary}${CRLF}` +
219
- `Content-Disposition: form-data; name="${sanitizeHeaderParam(part.name)}"${CRLF}${CRLF}` +
220
- `${part.value}${CRLF}`, 'utf8'));
594
+ `Content-Disposition: form-data; name="${sanitizeHeaderParam(part.name)}"${CRLF}${CRLF}`, 'utf8'), valueBuf, Buffer.from(CRLF, 'utf8'));
221
595
  }
222
596
  else {
223
597
  chunks.push(Buffer.from(`--${boundary}${CRLF}` +
@@ -228,32 +602,4 @@ function encodeMultipart(parts, boundary) {
228
602
  chunks.push(Buffer.from(`--${boundary}--${CRLF}`, 'utf8'));
229
603
  return Buffer.concat(chunks);
230
604
  }
231
- /** Narrowing guard — a non-null, non-array object usable as a string map. */
232
- function isRecord(x) {
233
- return typeof x === 'object' && x !== null && !Array.isArray(x);
234
- }
235
- /**
236
- * Partial deep-match. For objects, every key in `expected` must match in
237
- * `actual`. For arrays, every element in `expected` must match SOMEWHERE in
238
- * `actual` (order-independent). Primitives compared by strict equality.
239
- */
240
- export function partialMatch(actual, expected) {
241
- if (expected === null || expected === undefined) {
242
- return actual === expected;
243
- }
244
- if (typeof expected !== 'object')
245
- return actual === expected;
246
- if (Array.isArray(expected)) {
247
- if (!Array.isArray(actual))
248
- return false;
249
- return expected.every((want) => actual.some((have) => partialMatch(have, want)));
250
- }
251
- if (!isRecord(actual) || !isRecord(expected))
252
- return false;
253
- for (const key of Object.keys(expected)) {
254
- if (!partialMatch(actual[key], expected[key]))
255
- return false;
256
- }
257
- return true;
258
- }
259
605
  //# sourceMappingURL=RequestBuilder.js.map