@vertz/testing 0.2.26 → 0.2.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -131,5 +131,77 @@ declare function createTestApp(config?: TestAppConfig): TestApp;
131
131
  * @returns A typed test app instance.
132
132
  */
133
133
  declare function createTestApp<TRouteMap extends RouteMapEntry>(config?: TestAppConfig): TestAppWithRoutes<TRouteMap>;
134
+ import { AppBuilder } from "@vertz/server";
135
+ import { ModelDef } from "@vertz/db";
136
+ import { EntityDefinition, ListResult, ServiceActionDef, ServiceDefinition } from "@vertz/server";
137
+ import { ListResult as ListResult2 } from "@vertz/server";
138
+ interface ErrorBody {
139
+ error: string;
140
+ message: string;
141
+ statusCode: number;
142
+ details?: unknown;
143
+ }
144
+ type TestResponse2<T = unknown> = {
145
+ ok: true;
146
+ status: number;
147
+ body: T;
148
+ headers: Record<string, string>;
149
+ raw: Response;
150
+ } | {
151
+ ok: false;
152
+ status: number;
153
+ body: ErrorBody;
154
+ headers: Record<string, string>;
155
+ raw: Response;
156
+ };
157
+ interface RequestOptions2<TBody = unknown> {
158
+ body?: TBody;
159
+ headers?: Record<string, string>;
160
+ }
161
+ interface EntityListOptions {
162
+ where?: Record<string, unknown>;
163
+ orderBy?: Record<string, "asc" | "desc">;
164
+ limit?: number;
165
+ after?: string;
166
+ select?: Record<string, true>;
167
+ include?: Record<string, unknown>;
168
+ headers?: Record<string, string>;
169
+ }
170
+ interface EntityRequestOptions {
171
+ headers?: Record<string, string>;
172
+ }
173
+ interface ServiceCallOptions {
174
+ headers?: Record<string, string>;
175
+ }
176
+ interface TestClientOptions {
177
+ defaultHeaders?: Record<string, string>;
178
+ }
179
+ interface EntityTestProxy<TModel extends ModelDef> {
180
+ list(options?: EntityListOptions): Promise<TestResponse2<ListResult<TModel["table"]["$response"]>>>;
181
+ get(id: string, options?: EntityRequestOptions): Promise<TestResponse2<TModel["table"]["$response"]>>;
182
+ create(body: TModel["table"]["$create_input"], options?: EntityRequestOptions): Promise<TestResponse2<TModel["table"]["$response"]>>;
183
+ update(id: string, body: TModel["table"]["$update_input"], options?: EntityRequestOptions): Promise<TestResponse2<TModel["table"]["$response"]>>;
184
+ delete(id: string, options?: EntityRequestOptions): Promise<TestResponse2<null>>;
185
+ }
186
+ /** Extract TActions from a ServiceDefinition's phantom type field */
187
+ type InferActions<TDef> = TDef extends {
188
+ readonly __actions?: infer A;
189
+ } ? A extends Record<string, ServiceActionDef<infer _I, infer _O, infer _C>> ? A : Record<string, ServiceActionDef> : Record<string, ServiceActionDef>;
190
+ type ExtractInput<T> = T extends ServiceActionDef<infer TInput, infer _O, infer _C> ? TInput : unknown;
191
+ type ExtractOutput<T> = T extends ServiceActionDef<infer _I, infer TOutput, infer _C> ? TOutput : unknown;
192
+ type ServiceTestProxy<TDef extends ServiceDefinition> = { [K in Extract<keyof InferActions<TDef>, string>] : [unknown] extends [ExtractInput<InferActions<TDef>[K]>] ? (options?: ServiceCallOptions) => Promise<TestResponse2<ExtractOutput<InferActions<TDef>[K]>>> : (body: ExtractInput<InferActions<TDef>[K]>, options?: ServiceCallOptions) => Promise<TestResponse2<ExtractOutput<InferActions<TDef>[K]>>> };
193
+ interface TestClient {
194
+ entity<TModel extends ModelDef>(def: EntityDefinition<TModel>): EntityTestProxy<TModel>;
195
+ service<TDef extends ServiceDefinition>(def: TDef): ServiceTestProxy<TDef>;
196
+ /** Returns a new immutable client with merged default headers. */
197
+ withHeaders(headers: Record<string, string>): TestClient;
198
+ get(path: string, options?: RequestOptions2): Promise<TestResponse2>;
199
+ post(path: string, options?: RequestOptions2): Promise<TestResponse2>;
200
+ put(path: string, options?: RequestOptions2): Promise<TestResponse2>;
201
+ patch(path: string, options?: RequestOptions2): Promise<TestResponse2>;
202
+ delete(path: string, options?: RequestOptions2): Promise<TestResponse2>;
203
+ head(path: string, options?: RequestOptions2): Promise<TestResponse2>;
204
+ }
205
+ declare function createTestClient(server: AppBuilder, options?: TestClientOptions): TestClient;
134
206
  type DeepPartial<T> = { [P in keyof T]? : T[P] extends object ? DeepPartial<T[P]> : T[P] };
135
- export { createTestApp, TestRouteEntry, TestResponse, TestRequestBuilder, TestAppConfig, TestApp, RouteMapEntry, RequestOptions, DeepPartial };
207
+ export { createTestClient, createTestApp, TestResponse2 as TypedTestResponse, TestRouteEntry, TestResponse, TestRequestBuilder, TestClientOptions, TestClient, TestAppConfig, TestApp, ServiceTestProxy, ServiceCallOptions, RouteMapEntry, RequestOptions, ListResult2 as ListResult, ErrorBody, EntityTestProxy, EntityRequestOptions, EntityListOptions, DeepPartial };
package/dist/index.js CHANGED
@@ -157,6 +157,180 @@ class ResponseValidationError extends Error {
157
157
  this.name = "ResponseValidationError";
158
158
  }
159
159
  }
160
+ // src/test-client.ts
161
+ function resolveEntityBasePath(server, entityName) {
162
+ const routes = server.router.routes;
163
+ const suffix = `/${entityName}`;
164
+ const listRoute = routes.find((r) => r.method === "GET" && (r.path === suffix || r.path.endsWith(suffix)));
165
+ if (listRoute)
166
+ return listRoute.path;
167
+ return `/api/${entityName}`;
168
+ }
169
+ function resolveServiceBasePath(server, serviceName) {
170
+ const routes = server.router.routes;
171
+ const segmentPattern = new RegExp(`/${serviceName}/`);
172
+ const serviceRoute = routes.find((r) => r.method === "POST" && segmentPattern.test(r.path));
173
+ if (serviceRoute) {
174
+ const idx = serviceRoute.path.indexOf(`/${serviceName}/`);
175
+ return serviceRoute.path.slice(0, idx + 1 + serviceName.length);
176
+ }
177
+ return `/api/${serviceName}`;
178
+ }
179
+ async function parseResponse(raw) {
180
+ const status = raw.status;
181
+ const ok = raw.ok;
182
+ const headers = {};
183
+ raw.headers.forEach((value, key) => {
184
+ headers[key] = value;
185
+ });
186
+ let body;
187
+ const contentType = raw.headers.get("content-type");
188
+ if (contentType?.includes("application/json")) {
189
+ body = await raw.json();
190
+ } else if (status === 204 || raw.headers.get("content-length") === "0") {
191
+ body = null;
192
+ } else {
193
+ try {
194
+ body = await raw.json();
195
+ } catch {
196
+ body = null;
197
+ }
198
+ }
199
+ return { ok, status, body, headers, raw };
200
+ }
201
+ function getRequestHandler(server) {
202
+ if ("requestHandler" in server && typeof server.requestHandler === "function") {
203
+ return server.requestHandler;
204
+ }
205
+ return server.handler;
206
+ }
207
+ async function dispatch(handler, method, path, defaultHeaders, options) {
208
+ const mergedHeaders = { ...defaultHeaders, ...options?.headers };
209
+ if (options?.body !== undefined) {
210
+ mergedHeaders["content-type"] = "application/json";
211
+ }
212
+ const request = new Request(`http://localhost${path}`, {
213
+ method,
214
+ body: options?.body !== undefined ? JSON.stringify(options.body) : undefined,
215
+ headers: mergedHeaders
216
+ });
217
+ const raw = await handler(request);
218
+ return parseResponse(raw);
219
+ }
220
+ function createEntityProxy(handler, basePath, defaultHeaders) {
221
+ return {
222
+ list(options) {
223
+ const params = new URLSearchParams;
224
+ if (options?.where)
225
+ params.set("where", JSON.stringify(options.where));
226
+ if (options?.orderBy)
227
+ params.set("orderBy", JSON.stringify(options.orderBy));
228
+ if (options?.limit !== undefined)
229
+ params.set("limit", String(options.limit));
230
+ if (options?.after)
231
+ params.set("after", options.after);
232
+ if (options?.select)
233
+ params.set("select", JSON.stringify(options.select));
234
+ if (options?.include)
235
+ params.set("include", JSON.stringify(options.include));
236
+ const qs = params.toString();
237
+ const path = qs ? `${basePath}?${qs}` : basePath;
238
+ return dispatch(handler, "GET", path, { ...defaultHeaders, ...options?.headers });
239
+ },
240
+ get(id, options) {
241
+ return dispatch(handler, "GET", `${basePath}/${id}`, {
242
+ ...defaultHeaders,
243
+ ...options?.headers
244
+ });
245
+ },
246
+ create(body, options) {
247
+ return dispatch(handler, "POST", basePath, { ...defaultHeaders, ...options?.headers }, {
248
+ body
249
+ });
250
+ },
251
+ update(id, body, options) {
252
+ return dispatch(handler, "PATCH", `${basePath}/${id}`, {
253
+ ...defaultHeaders,
254
+ ...options?.headers
255
+ }, { body });
256
+ },
257
+ delete(id, options) {
258
+ return dispatch(handler, "DELETE", `${basePath}/${id}`, {
259
+ ...defaultHeaders,
260
+ ...options?.headers
261
+ });
262
+ }
263
+ };
264
+ }
265
+ function createServiceProxy(handler, basePath, serviceDef, defaultHeaders) {
266
+ const actionNames = Object.keys(serviceDef.actions);
267
+ return new Proxy({}, {
268
+ get(_target, prop) {
269
+ if (typeof prop !== "string")
270
+ return;
271
+ if (!actionNames.includes(prop))
272
+ return;
273
+ return (...args) => {
274
+ const action = serviceDef.actions[prop];
275
+ const hasBody = action?.body !== undefined;
276
+ let body;
277
+ let options;
278
+ if (hasBody) {
279
+ body = args[0];
280
+ options = args[1];
281
+ } else {
282
+ options = args[0];
283
+ }
284
+ const method = action?.method?.toUpperCase() ?? "POST";
285
+ const actionPath = action?.path ?? `/${prop}`;
286
+ const fullPath = `${basePath}${actionPath}`;
287
+ return dispatch(handler, method, fullPath, {
288
+ ...defaultHeaders,
289
+ ...options?.headers
290
+ }, body !== undefined ? { body } : undefined);
291
+ };
292
+ }
293
+ });
294
+ }
295
+ function createTestClient(server, options) {
296
+ const defaultHeaders = options?.defaultHeaders ?? {};
297
+ const handler = getRequestHandler(server);
298
+ function makeClient(headers) {
299
+ return {
300
+ entity(def) {
301
+ const basePath = resolveEntityBasePath(server, def.name);
302
+ return createEntityProxy(handler, basePath, headers);
303
+ },
304
+ service(def) {
305
+ const basePath = resolveServiceBasePath(server, def.name);
306
+ return createServiceProxy(handler, basePath, def, headers);
307
+ },
308
+ withHeaders(newHeaders) {
309
+ return makeClient({ ...headers, ...newHeaders });
310
+ },
311
+ get(path, opts) {
312
+ return dispatch(handler, "GET", path, headers, opts);
313
+ },
314
+ post(path, opts) {
315
+ return dispatch(handler, "POST", path, headers, opts);
316
+ },
317
+ put(path, opts) {
318
+ return dispatch(handler, "PUT", path, headers, opts);
319
+ },
320
+ patch(path, opts) {
321
+ return dispatch(handler, "PATCH", path, headers, opts);
322
+ },
323
+ delete(path, opts) {
324
+ return dispatch(handler, "DELETE", path, headers, opts);
325
+ },
326
+ head(path, opts) {
327
+ return dispatch(handler, "HEAD", path, headers, opts);
328
+ }
329
+ };
330
+ }
331
+ return makeClient(defaultHeaders);
332
+ }
160
333
  export {
334
+ createTestClient,
161
335
  createTestApp
162
336
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vertz/testing",
3
- "version": "0.2.26",
3
+ "version": "0.2.29",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Testing utilities for Vertz applications",
@@ -30,12 +30,13 @@
30
30
  "typecheck": "tsc --noEmit"
31
31
  },
32
32
  "dependencies": {
33
- "@vertz/core": "^0.2.25",
34
- "@vertz/server": "^0.2.25"
33
+ "@vertz/core": "^0.2.28",
34
+ "@vertz/db": "^0.2.28",
35
+ "@vertz/server": "^0.2.28"
35
36
  },
36
37
  "devDependencies": {
37
38
  "@types/node": "^25.3.1",
38
- "@vertz/schema": "^0.2.25",
39
+ "@vertz/schema": "^0.2.28",
39
40
  "bunup": "^0.16.31",
40
41
  "typescript": "^5.7.0"
41
42
  },