Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

crosswalk

Package Overview
Dependencies
Maintainers
1
Versions
14
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

crosswalk - npm Package Compare versions

Comparing version 1.3.0 to 1.4.0

8

CHANGELOG.md
# Changelog
## 1.4
- Type validation for query parameters [#19](https://github.com/danvk/crosswalk/issues/19).
Thanks to @CarterGrimmeisen for the contribution!
- Security update for `y18n` [#20](https://github.com/danvk/crosswalk/pull/20)
- Add an option for a custom 400 Invalid Request handler [#19](https://github.com/danvk/crosswalk/pull/19)
- Add `--noExtraProps` to README with some explanatory Q&A.
## 1.3

@@ -4,0 +12,0 @@

7

dist/api-spec.d.ts

@@ -1,11 +0,12 @@

export interface Endpoint<Request, Response> {
export interface Endpoint<Request, Response, Query = null> {
request: Request;
response: Response;
query: Query;
}
export declare type GetEndpoint<Response> = Endpoint<null, Response>;
export declare type GetEndpoint<Response, Query = null> = Endpoint<null, Response, Query>;
export declare type HTTPVerb = 'get' | 'post' | 'put' | 'delete' | 'patch';
export interface APISpec {
[path: string]: {
[method in HTTPVerb]?: Endpoint<any, any>;
[method in HTTPVerb]?: Endpoint<any, any, any>;
};
}

@@ -77,3 +77,3 @@ "use strict";

var _d = followApiRef(apiSpec, ref), name_1 = _d[0], schema = _d[1];
var _e = schema.properties, request = _e.request, response = _e.response;
var _e = schema.properties, request = _e.request, response = _e.response, query = _e.query;
var parameters = extractPathParams(endpoint);

@@ -87,2 +87,12 @@ if ((request === null || request === void 0 ? void 0 : request.type) !== 'null') {

}
if (query.properties) {
for (var _f = 0, _g = Object.entries(query.properties); _f < _g.length; _f++) {
var _h = _g[_f], key = _h[0], value = _h[1];
parameters.push({
name: key,
in: 'query',
schema: value,
});
}
}
var swagger = __assign(__assign({ summary: ref.description }, (parameters.length && { parameters: parameters })), { responses: {

@@ -89,0 +99,0 @@ // TODO: do I need to break this down by status?

@@ -6,13 +6,65 @@ "use strict";

exports.STATUS_CODES = [
100, 101, 102, 103, 200, 201,
202, 203, 204, 205, 206, 207,
208, 226, 300, 301, 302, 303,
304, 305, 307, 308, 400, 401,
402, 403, 404, 405, 406, 407,
408, 409, 410, 411, 412, 413,
414, 415, 416, 417, 418, 421,
422, 423, 424, 425, 426, 428,
429, 431, 451, 500, 501, 502,
503, 504, 505, 506, 507, 508,
509, 510, 511
100,
101,
102,
103,
200,
201,
202,
203,
204,
205,
206,
207,
208,
226,
300,
301,
302,
303,
304,
305,
307,
308,
400,
401,
402,
403,
404,
405,
406,
407,
408,
409,
410,
411,
412,
413,
414,
415,
416,
417,
418,
421,
422,
423,
424,
425,
426,
428,
429,
431,
451,
500,
501,
502,
503,
504,
505,
506,
507,
508,
509,
510,
511,
];
/** Type-safe wrapper around fetch() for REST APIs */
import { HTTPVerb } from './api-spec';
import { ExtractRouteParams, SafeKey, DeepReadonly, PathsForMethod } from './utils';
declare type ExtractRouteParamsVarArgs<T extends string> = {} extends ExtractRouteParams<T> ? [] : [params: Readonly<ExtractRouteParams<T>>];
declare type PlaceholderEmpty = null | {
[pathParam: string]: never;
};
declare type ParamVarArgs<Params, Query> = DeepReadonly<[
Query
] extends [null] ? [{}] extends [Params] ? [] : [params: Params] : [{}, {}] extends [Params, Query] ? [
params?: PlaceholderEmpty,
query?: Query
] : [
params: [{}] extends Params ? PlaceholderEmpty : Params,
...query: [{}] extends [Query] ? [query?: Query] : [query: Query]
]>;
/** Utility for safely constructing API URLs */
export declare function apiUrlMaker<API>(prefix?: string): <Path extends keyof API>(endpoint: Path & string) => (...paramsList: ExtractRouteParamsVarArgs<Path & string>) => string;
export declare function apiUrlMaker<API>(prefix?: string): <Args extends [endpoint: keyof API, method?: AllMethods | undefined], Path extends Args[0] = Args[0], P extends API[Path] = API[Path], AllMethods extends keyof P = keyof P>(...[endpoint, _method]: Args) => (...paramsList: DeepReadonly<[SafeKey<P, Args[1] extends undefined ? "get" : Args[1]>["query" & keyof SafeKey<P, Args[1] extends undefined ? "get" : Args[1]>]] extends [null] ? [{}] extends [string extends Path & string ? Record<Path & string, Path & string> : Path & string extends `${infer _Start}:${infer Param}/${infer Rest}` ? { [k in Param | keyof ExtractRouteParams<Rest>]: string; } : Path & string extends `${infer _Start_1}:${infer Param_1}` ? { [k_1 in Param_1]: string; } : {}] ? [] : [params: string extends Path & string ? Record<Path & string, Path & string> : Path & string extends `${infer _Start}:${infer Param}/${infer Rest}` ? { [k in Param | keyof ExtractRouteParams<Rest>]: string; } : Path & string extends `${infer _Start_1}:${infer Param_1}` ? { [k_1 in Param_1]: string; } : {}] : [{}, {}] extends [string extends Path & string ? Record<Path & string, Path & string> : Path & string extends `${infer _Start}:${infer Param}/${infer Rest}` ? { [k in Param | keyof ExtractRouteParams<Rest>]: string; } : Path & string extends `${infer _Start_1}:${infer Param_1}` ? { [k_1 in Param_1]: string; } : {}, SafeKey<P, Args[1] extends undefined ? "get" : Args[1]>["query" & keyof SafeKey<P, Args[1] extends undefined ? "get" : Args[1]>]] ? [params?: PlaceholderEmpty | undefined, query?: SafeKey<P, Args[1] extends undefined ? "get" : Args[1]>["query" & keyof SafeKey<P, Args[1] extends undefined ? "get" : Args[1]>] | undefined] : [params: [{}] extends (string extends Path & string ? Record<Path & string, Path & string> : Path & string extends `${infer _Start}:${infer Param}/${infer Rest}` ? { [k in Param | keyof ExtractRouteParams<Rest>]: string; } : Path & string extends `${infer _Start_1}:${infer Param_1}` ? { [k_1 in Param_1]: string; } : {}) ? PlaceholderEmpty : string extends Path & string ? Record<Path & string, Path & string> : Path & string extends `${infer _Start}:${infer Param}/${infer Rest}` ? { [k in Param | keyof ExtractRouteParams<Rest>]: string; } : Path & string extends `${infer _Start_1}:${infer Param_1}` ? { [k_1 in Param_1]: string; } : {}, ...query: [{}] extends [SafeKey<P, Args[1] extends undefined ? "get" : Args[1]>["query" & keyof SafeKey<P, Args[1] extends undefined ? "get" : Args[1]>]] ? [query?: SafeKey<P, Args[1] extends undefined ? "get" : Args[1]>["query" & keyof SafeKey<P, Args[1] extends undefined ? "get" : Args[1]>] | undefined] : [query: SafeKey<P, Args[1] extends undefined ? "get" : Args[1]>["query" & keyof SafeKey<P, Args[1] extends undefined ? "get" : Args[1]>]]]>) => string;
export interface Options {

@@ -15,9 +26,11 @@ /** Prefix to add to all API endpoints (e.g. /api/v0) */

export declare function typedApi<API>(options?: Options): {
get: <Path extends PathsForMethod<API, "get">>(endpoint: Path) => (...params: ExtractRouteParamsVarArgs<Path & string>) => Promise<SafeKey<SafeKey<API[Path], "get">, "response">>;
delete: <Path_1 extends PathsForMethod<API, "delete">>(endpoint: Path_1) => (...params: ExtractRouteParamsVarArgs<Path_1 & string>) => Promise<SafeKey<SafeKey<API[Path_1], "delete">, "response">>;
post: <Path_2 extends PathsForMethod<API, "post">>(endpoint: Path_2) => (queryParams: ExtractRouteParams<Path_2 & string>, body: DeepReadonly<SafeKey<SafeKey<API[Path_2], "post">, "request">>) => Promise<SafeKey<SafeKey<API[Path_2], "post">, "response">>;
patch: <Path_3 extends PathsForMethod<API, "patch">>(endpoint: Path_3) => (queryParams: ExtractRouteParams<Path_3 & string>, body: DeepReadonly<SafeKey<SafeKey<API[Path_3], "patch">, "request">>) => Promise<SafeKey<SafeKey<API[Path_3], "patch">, "response">>;
put: <Path_4 extends PathsForMethod<API, "put">>(endpoint: Path_4) => (queryParams: ExtractRouteParams<Path_4 & string>, body: DeepReadonly<SafeKey<SafeKey<API[Path_4], "put">, "request">>) => Promise<SafeKey<SafeKey<API[Path_4], "put">, "response">>;
request: <Method extends HTTPVerb>(method: Method, path: PathsForMethod<API, Method>) => (queryParams: ExtractRouteParams<PathsForMethod<API, Method>>, body: DeepReadonly<SafeKey<SafeKey<API[PathsForMethod<API, Method>], Method>, "request">>) => Promise<SafeKey<SafeKey<API[PathsForMethod<API, Method>], Method>, "response">>;
get: <Path extends PathsForMethod<API, "get">>(endpoint: Path) => (...params: DeepReadonly<[API[Path]["get" & keyof API[Path]]["query" & keyof API[Path]["get" & keyof API[Path]]]] extends [null] ? [{}] extends [ExtractRouteParams<Path & string>] ? [] : [params: ExtractRouteParams<Path & string>] : [{}, {}] extends [ExtractRouteParams<Path & string>, API[Path]["get" & keyof API[Path]]["query" & keyof API[Path]["get" & keyof API[Path]]]] ? [params?: PlaceholderEmpty | undefined, query?: API[Path]["get" & keyof API[Path]]["query" & keyof API[Path]["get" & keyof API[Path]]] | undefined] : [params: [{}] extends ExtractRouteParams<Path & string> ? PlaceholderEmpty : ExtractRouteParams<Path & string>, ...query: [{}] extends [API[Path]["get" & keyof API[Path]]["query" & keyof API[Path]["get" & keyof API[Path]]]] ? [query?: API[Path]["get" & keyof API[Path]]["query" & keyof API[Path]["get" & keyof API[Path]]] | undefined] : [query: API[Path]["get" & keyof API[Path]]["query" & keyof API[Path]["get" & keyof API[Path]]]]]>) => Promise<API[Path]["get" & keyof API[Path]]["response" & keyof API[Path]["get" & keyof API[Path]]]>;
delete: <Path_1 extends PathsForMethod<API, "delete">>(endpoint: Path_1) => (...params: DeepReadonly<[API[Path_1]["delete" & keyof API[Path_1]]["query" & keyof API[Path_1]["delete" & keyof API[Path_1]]]] extends [null] ? [{}] extends [ExtractRouteParams<Path_1 & string>] ? [] : [params: ExtractRouteParams<Path_1 & string>] : [{}, {}] extends [ExtractRouteParams<Path_1 & string>, API[Path_1]["delete" & keyof API[Path_1]]["query" & keyof API[Path_1]["delete" & keyof API[Path_1]]]] ? [params?: PlaceholderEmpty | undefined, query?: API[Path_1]["delete" & keyof API[Path_1]]["query" & keyof API[Path_1]["delete" & keyof API[Path_1]]] | undefined] : [params: [{}] extends ExtractRouteParams<Path_1 & string> ? PlaceholderEmpty : ExtractRouteParams<Path_1 & string>, ...query: [{}] extends [API[Path_1]["delete" & keyof API[Path_1]]["query" & keyof API[Path_1]["delete" & keyof API[Path_1]]]] ? [query?: API[Path_1]["delete" & keyof API[Path_1]]["query" & keyof API[Path_1]["delete" & keyof API[Path_1]]] | undefined] : [query: API[Path_1]["delete" & keyof API[Path_1]]["query" & keyof API[Path_1]["delete" & keyof API[Path_1]]]]]>) => Promise<API[Path_1]["delete" & keyof API[Path_1]]["response" & keyof API[Path_1]["delete" & keyof API[Path_1]]]>;
post: <Path_2 extends PathsForMethod<API, "post">>(endpoint: Path_2) => (params: ExtractRouteParams<Path_2 & string>, body: DeepReadonly<SafeKey<API[Path_2]["post" & keyof API[Path_2]], "request">>, ...queryArgs: [API[Path_2]["post" & keyof API[Path_2]]["query" & keyof API[Path_2]["post" & keyof API[Path_2]]]] extends [{}] ? [] : [query?: API[Path_2]["post" & keyof API[Path_2]]["query" & keyof API[Path_2]["post" & keyof API[Path_2]]] | undefined]) => Promise<API[Path_2]["post" & keyof API[Path_2]]["response" & keyof API[Path_2]["post" & keyof API[Path_2]]]>;
patch: <Path_3 extends PathsForMethod<API, "patch">>(endpoint: Path_3) => (params: ExtractRouteParams<Path_3 & string>, body: DeepReadonly<SafeKey<API[Path_3]["patch" & keyof API[Path_3]], "request">>, ...queryArgs: [API[Path_3]["patch" & keyof API[Path_3]]["query" & keyof API[Path_3]["patch" & keyof API[Path_3]]]] extends [{}] ? [] : [query?: API[Path_3]["patch" & keyof API[Path_3]]["query" & keyof API[Path_3]["patch" & keyof API[Path_3]]] | undefined]) => Promise<API[Path_3]["patch" & keyof API[Path_3]]["response" & keyof API[Path_3]["patch" & keyof API[Path_3]]]>;
put: <Path_4 extends PathsForMethod<API, "put">>(endpoint: Path_4) => (params: ExtractRouteParams<Path_4 & string>, body: DeepReadonly<SafeKey<API[Path_4]["put" & keyof API[Path_4]], "request">>, ...queryArgs: [API[Path_4]["put" & keyof API[Path_4]]["query" & keyof API[Path_4]["put" & keyof API[Path_4]]]] extends [{}] ? [] : [query?: API[Path_4]["put" & keyof API[Path_4]]["query" & keyof API[Path_4]["put" & keyof API[Path_4]]] | undefined]) => Promise<API[Path_4]["put" & keyof API[Path_4]]["response" & keyof API[Path_4]["put" & keyof API[Path_4]]]>;
request: <Method extends HTTPVerb>(method: Method, path: PathsForMethod<API, Method>) => (params: ExtractRouteParams<Extract<import("./utils").Unionize<API>, {
v: Record<Method, any>;
}>["k"] & keyof API & string>, body: DeepReadonly<SafeKey<API[PathsForMethod<API, Method>][Method & keyof API[PathsForMethod<API, Method>]], "request">>, ...queryArgs: [API[PathsForMethod<API, Method>][Method & keyof API[PathsForMethod<API, Method>]]["query" & keyof API[PathsForMethod<API, Method>][Method & keyof API[PathsForMethod<API, Method>]]]] extends [{}] ? [] : [query?: API[PathsForMethod<API, Method>][Method & keyof API[PathsForMethod<API, Method>]]["query" & keyof API[PathsForMethod<API, Method>][Method & keyof API[PathsForMethod<API, Method>]]] | undefined]) => Promise<API[PathsForMethod<API, Method>][Method & keyof API[PathsForMethod<API, Method>]]["response" & keyof API[PathsForMethod<API, Method>][Method & keyof API[PathsForMethod<API, Method>]]]>;
};
export {};

@@ -45,3 +45,9 @@ "use strict";

if (prefix === void 0) { prefix = ''; }
return function (endpoint) {
/** This assumes GET for the type of the query params unless another method is specified. */
return function () {
var _a = [];
for (var _i = 0; _i < arguments.length; _i++) {
_a[_i] = arguments[_i];
}
var endpoint = _a[0], _method = _a[1];
var toPath = path_to_regexp_1.compile(endpoint);

@@ -53,3 +59,5 @@ return function () {

}
return prefix + toPath(paramsList[0]);
var params = paramsList;
var queryString = params[1] ? '' + new URLSearchParams(params[1]) : '';
return prefix + toPath(params[0]) + (queryString ? '?' + queryString : '');
};

@@ -86,5 +94,10 @@ };

var requestWithBody = function (method) { return function (endpoint) {
var makeUrl = urlMaker(endpoint);
return function (queryParams, body) {
return fetcher(makeUrl(queryParams), method, body);
var makeUrl = urlMaker(endpoint, method);
return function () {
var _a = [];
for (var _i = 0; _i < arguments.length; _i++) {
_a[_i] = arguments[_i];
}
var params = _a[0], body = _a[1], query = _a[2];
return fetcher(makeUrl(params, query), method, body);
};

@@ -98,3 +111,3 @@ }; };

}
return requestWithBody(method)(endpoint)(params === null || params === void 0 ? void 0 : params[0], null);
return requestWithBody(method)(endpoint)(params === null || params === void 0 ? void 0 : params[0], null, params === null || params === void 0 ? void 0 : params[1]);
};

@@ -101,0 +114,0 @@ }; };

/** Type-safe wrapper around Express router for REST APIs */
/// <reference types="qs" />
import Ajv from 'ajv';

@@ -13,5 +12,21 @@ import express from 'express';

}
declare type AnyEndpoint = Endpoint<any, any>;
declare type ExpressRequest<Path extends string, Spec> = unknown & express.Request<ExtractRouteParams<Path>, SafeKey<Spec, 'response'>, SafeKey<Spec, 'request'>>;
declare type AnyEndpoint = Endpoint<any, any, any>;
declare type ExpressRequest<Path extends string, Spec> = unknown & express.Request<ExtractRouteParams<Path>, SafeKey<Spec, 'response'>, SafeKey<Spec, 'request'>, SafeKey<Spec, 'query'>>;
declare type ExpressResponse<Spec> = unknown & express.Response<SafeKey<Spec, 'response'>>;
export interface InvalidRequestHandlerArgs {
/** Which part of the request was invalid (request body or query parameters)? */
which: 'body' | 'query';
request: express.Request;
response: express.Response;
/** The invalid payload object */
payload: unknown;
/** Ajv validator that found the problem. */
ajv: Ajv.Ajv;
/** List of errors with the payload, as reported by Ajv. */
errors: Ajv.ErrorObject[];
}
export declare function defaultInvalidRequestHandler({ response, payload, ajv, errors, }: InvalidRequestHandlerArgs): void;
export interface TypedRouterOptions {
invalidRequestHandler: (obj: InvalidRequestHandlerArgs) => void;
}
export declare class TypedRouter<API> {

@@ -25,12 +40,13 @@ router: express.Router;

}[];
constructor(router: express.Router, apiSchema?: any);
get: <Path extends PathsForMethod<API, "get">, Spec extends SafeKey<API[Path], "get"> = SafeKey<API[Path], "get">>(route: Path, handler: (params: ExtractRouteParams<Path>, request: express.Request<ExtractRouteParams<Path>, SafeKey<Spec, "response">, SafeKey<Spec, "request">, import("qs").ParsedQs, Record<string, any>>, response: express.Response<SafeKey<Spec, "response">, Record<string, any>>) => Promise<Spec extends AnyEndpoint ? Spec["response"] : never>) => void;
delete: <Path extends PathsForMethod<API, "delete">, Spec extends SafeKey<API[Path], "delete"> = SafeKey<API[Path], "delete">>(route: Path, handler: (params: ExtractRouteParams<Path>, request: express.Request<ExtractRouteParams<Path>, SafeKey<Spec, "response">, SafeKey<Spec, "request">, import("qs").ParsedQs, Record<string, any>>, response: express.Response<SafeKey<Spec, "response">, Record<string, any>>) => Promise<Spec extends AnyEndpoint ? Spec["response"] : never>) => void;
post: <Path extends PathsForMethod<API, "post">, Spec extends SafeKey<API[Path], "post"> = SafeKey<API[Path], "post">>(route: Path, handler: (params: ExtractRouteParams<Path>, body: SafeKey<Spec, "request">, request: express.Request<ExtractRouteParams<Path>, SafeKey<Spec, "response">, SafeKey<Spec, "request">, import("qs").ParsedQs, Record<string, any>>, response: express.Response<SafeKey<Spec, "response">, Record<string, any>>) => Promise<Spec extends AnyEndpoint ? Spec["response"] : never>) => void;
patch: <Path extends PathsForMethod<API, "patch">, Spec extends SafeKey<API[Path], "patch"> = SafeKey<API[Path], "patch">>(route: Path, handler: (params: ExtractRouteParams<Path>, body: SafeKey<Spec, "request">, request: express.Request<ExtractRouteParams<Path>, SafeKey<Spec, "response">, SafeKey<Spec, "request">, import("qs").ParsedQs, Record<string, any>>, response: express.Response<SafeKey<Spec, "response">, Record<string, any>>) => Promise<Spec extends AnyEndpoint ? Spec["response"] : never>) => void;
put: <Path extends PathsForMethod<API, "put">, Spec extends SafeKey<API[Path], "put"> = SafeKey<API[Path], "put">>(route: Path, handler: (params: ExtractRouteParams<Path>, body: SafeKey<Spec, "request">, request: express.Request<ExtractRouteParams<Path>, SafeKey<Spec, "response">, SafeKey<Spec, "request">, import("qs").ParsedQs, Record<string, any>>, response: express.Response<SafeKey<Spec, "response">, Record<string, any>>) => Promise<Spec extends AnyEndpoint ? Spec["response"] : never>) => void;
handleInvalidRequest: TypedRouterOptions['invalidRequestHandler'];
constructor(router: express.Router, apiSchema?: any, options?: TypedRouterOptions);
get: <Path extends PathsForMethod<API, "get">, Spec extends SafeKey<API[Path], "get"> = SafeKey<API[Path], "get">>(route: Path, handler: (params: ExtractRouteParams<Path>, request: express.Request<ExtractRouteParams<Path>, SafeKey<Spec, "response">, SafeKey<Spec, "request">, SafeKey<Spec, "query">, Record<string, any>>, response: express.Response<SafeKey<Spec, "response">, Record<string, any>>) => Promise<Spec extends AnyEndpoint ? Spec["response"] : never>) => void;
delete: <Path extends PathsForMethod<API, "delete">, Spec extends SafeKey<API[Path], "delete"> = SafeKey<API[Path], "delete">>(route: Path, handler: (params: ExtractRouteParams<Path>, request: express.Request<ExtractRouteParams<Path>, SafeKey<Spec, "response">, SafeKey<Spec, "request">, SafeKey<Spec, "query">, Record<string, any>>, response: express.Response<SafeKey<Spec, "response">, Record<string, any>>) => Promise<Spec extends AnyEndpoint ? Spec["response"] : never>) => void;
post: <Path extends PathsForMethod<API, "post">, Spec extends SafeKey<API[Path], "post"> = SafeKey<API[Path], "post">>(route: Path, handler: (params: ExtractRouteParams<Path>, body: SafeKey<Spec, "request">, request: express.Request<ExtractRouteParams<Path>, SafeKey<Spec, "response">, SafeKey<Spec, "request">, SafeKey<Spec, "query">, Record<string, any>>, response: express.Response<SafeKey<Spec, "response">, Record<string, any>>) => Promise<Spec extends AnyEndpoint ? Spec["response"] : never>) => void;
patch: <Path extends PathsForMethod<API, "patch">, Spec extends SafeKey<API[Path], "patch"> = SafeKey<API[Path], "patch">>(route: Path, handler: (params: ExtractRouteParams<Path>, body: SafeKey<Spec, "request">, request: express.Request<ExtractRouteParams<Path>, SafeKey<Spec, "response">, SafeKey<Spec, "request">, SafeKey<Spec, "query">, Record<string, any>>, response: express.Response<SafeKey<Spec, "response">, Record<string, any>>) => Promise<Spec extends AnyEndpoint ? Spec["response"] : never>) => void;
put: <Path extends PathsForMethod<API, "put">, Spec extends SafeKey<API[Path], "put"> = SafeKey<API[Path], "put">>(route: Path, handler: (params: ExtractRouteParams<Path>, body: SafeKey<Spec, "request">, request: express.Request<ExtractRouteParams<Path>, SafeKey<Spec, "response">, SafeKey<Spec, "request">, SafeKey<Spec, "query">, Record<string, any>>, response: express.Response<SafeKey<Spec, "response">, Record<string, any>>) => Promise<Spec extends AnyEndpoint ? Spec["response"] : never>) => void;
/** Register a handler on the router for the given path and verb */
registerEndpoint<Method extends HTTPVerb, Path extends PathsForMethod<API, Method>, Spec extends SafeKey<API[Path], Method> = SafeKey<API[Path], Method>>(method: Method, route: Path, handler: (params: ExtractRouteParams<Path>, body: SafeKey<Spec, 'request'>, request: ExpressRequest<Path, Spec>, response: ExpressResponse<Spec>) => Promise<Spec extends AnyEndpoint ? Spec['response'] : never>): void;
/** Get a validation function for request bodies for the endpoint, or null if not applicable. */
getValidator(route: string, method: HTTPVerb): Ajv.ValidateFunction | null;
getValidator(route: string, method: HTTPVerb, property: 'request' | 'query'): Ajv.ValidateFunction | null;
/** Throw if any routes declared in the API spec have not been implemented. */

@@ -37,0 +53,0 @@ assertAllRoutesRegistered(): void;

@@ -11,2 +11,4 @@ "use strict";

return function (d, b) {
if (typeof b !== "function" && b !== null)
throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
extendStatics(d, b);

@@ -32,3 +34,3 @@ function __() { this.constructor = d; }

Object.defineProperty(exports, "__esModule", { value: true });
exports.TypedRouter = exports.HTTPError = void 0;
exports.TypedRouter = exports.defaultInvalidRequestHandler = exports.HTTPError = void 0;
var ajv_1 = __importDefault(require("ajv"));

@@ -57,4 +59,14 @@ var status_codes_1 = require("./status-codes");

}; };
function defaultInvalidRequestHandler(_a) {
var response = _a.response, payload = _a.payload, ajv = _a.ajv, errors = _a.errors;
response.status(400).json({
error: ajv.errorsText(errors),
errors: errors,
invalidRequest: payload,
});
}
exports.defaultInvalidRequestHandler = defaultInvalidRequestHandler;
var TypedRouter = /** @class */ (function () {
function TypedRouter(router, apiSchema) {
function TypedRouter(router, apiSchema, options) {
var _a;
// TODO(danvk): consider replacing get() with a streamlined implementation

@@ -72,2 +84,3 @@ this.get = registerWithoutBody('get', this);

}
this.handleInvalidRequest = (_a = options === null || options === void 0 ? void 0 : options.invalidRequestHandler) !== null && _a !== void 0 ? _a : defaultInvalidRequestHandler;
this.registrations = [];

@@ -78,3 +91,4 @@ }

var _this = this;
var validate = this.getValidator(route, method);
var bodyValidate = this.getValidator(route, method, 'request');
var queryValidate = this.getValidator(route, method, 'query');
this.registrations.push({ path: route, method: method });

@@ -87,13 +101,26 @@ this.router[method](route, function () {

var req = _a[0], response = _a[1], next = _a[2];
var body = req.body;
if (validate && !validate(body)) {
return response.status(400).json({
error: _this.ajv.errorsText(validate.errors),
errors: validate.errors,
invalidRequest: body,
var body = req.body, query = req.query;
if (bodyValidate && !bodyValidate(body)) {
return _this.handleInvalidRequest({
which: 'body',
request: req,
response: response,
ajv: _this.ajv,
payload: body,
errors: bodyValidate.errors,
});
}
if (queryValidate && !queryValidate(query)) {
return _this.handleInvalidRequest({
which: 'query',
request: req,
response: response,
ajv: _this.ajv,
payload: query,
errors: queryValidate.errors,
});
}
if (req.app.get('env') === 'test') {
// eslint-disable-next-line no-console
console.debug(method, route, 'params=', req.params, 'body=', body);
console.debug(method, route, 'params=', req.params, 'body=', body, 'query=', query);
}

@@ -125,3 +152,3 @@ handler(req.params, body, req, response)

/** Get a validation function for request bodies for the endpoint, or null if not applicable. */
TypedRouter.prototype.getValidator = function (route, method) {
TypedRouter.prototype.getValidator = function (route, method, property) {
var _a;

@@ -139,22 +166,21 @@ var apiSchema = this.apiSchema;

var endpointTypes = apiSchema.definitions[endpoint].properties;
var requestType = endpointTypes.request;
if (requestType.$ref) {
requestType = requestType.$ref; // allow either references or inline types
var validateType = endpointTypes[property];
if (validateType.$ref) {
validateType = validateType.$ref; // allow either references or inline types
}
else if (requestType.type && requestType.type === 'null') {
requestType = null; // no request body, no validation
else if (validateType.type && validateType.type === 'null') {
validateType = null; // no request body, no validation
}
if (requestType && this.ajv) {
if (validateType && this.ajv) {
var validate = void 0;
if (typeof requestType === 'string') {
validate = (_a = this.ajv.getSchema(requestType)) !== null && _a !== void 0 ? _a : null;
if (typeof validateType === 'string') {
validate = (_a = this.ajv.getSchema(validateType)) !== null && _a !== void 0 ? _a : null;
}
else {
// Create a new AJV validate for inline object types.
// This assumes these will never reference other type definitions.
var requestAjv = new ajv_1.default();
validate = requestAjv.compile(__assign({ '$schema': apiSchema.$schema, definitions: apiSchema.definitions }, requestType));
var requestAjv = new ajv_1.default({ coerceTypes: property === 'query' });
validate = requestAjv.compile(__assign({ $schema: apiSchema.$schema, definitions: apiSchema.definitions }, validateType));
}
if (!validate) {
throw new Error("Unable to get schema for '" + requestType + "'");
throw new Error("Unable to get schema for '" + validateType + "'");
}

@@ -161,0 +187,0 @@ return validate;

{
"name": "crosswalk",
"version": "1.3.0",
"version": "1.4.0",
"description": "Type-safe express routing with TypeScript",

@@ -12,4 +12,4 @@ "main": "dist/index.js",

"test": "jest",
"lint": "prettier --check src/**/*.ts",
"prettier": "prettier --write src/**/*.ts",
"lint": "prettier --check 'src/**/*.ts'",
"prettier": "prettier --write 'src/**/*.ts'",
"update-test-schema": "typescript-json-schema --required --noExtraProps --strictNullChecks src/__tests__/api.ts API --out src/__tests__/api.schema.json"

@@ -16,0 +16,0 @@ },

@@ -47,3 +47,3 @@ # Crosswalk: safe routes for Express and TypeScript

'/users': {
get: GetEndpoint<UsersResponse>;
get: GetEndpoint<UsersResponse, {query?: string}>; // Response/query parameter types
post: Endpoint<CreateUserRequest, User>;

@@ -64,3 +64,3 @@ };

export function registerAPI(router: TypedRouter<API>) {
router.get('/users', async () => users;
router.get('/users', async ({}, req, res, {query}) => filterUsersByName(users, query));
router.post('/users', async ({}, userInput) => createUser(userInput));

@@ -86,2 +86,3 @@ router.get('/users/:userId', async ({userId}) => getUserById(userId));

- Types for route parameters (via TypeScript 4.1's template literal types)
- Types for query parameters (and automatic coercion of non-string parameters)
- A check that each endpoint's implementation returns a Promise for the

@@ -129,5 +130,8 @@ expected response type.

const urlMaker = apiUrlMaker<API>('/api/v0');
const getUserUrl = urlMaker('/users/:userId');
const fredUrl = getUserUrl({userId: 'fred'});
const getUserByIdUrl = urlMaker('/users/:userId');
const fredUrl = getUserByIdUrl({userId: 'fred'});
// /api/v0/users/fred
const userUrl = urlMaker('/users');
const fredSearchUrl = userUrl(null, {query: 'fred'});
// /api/v0/users?query=fred
```

@@ -140,3 +144,3 @@

typescript-json-schema --required --strictNullChecks api.ts API --out api.schema.json
typescript-json-schema --required --strictNullChecks --noExtraProps api.ts API --out api.schema.json

@@ -224,2 +228,26 @@ Then pass this to the `TypeRouter` when you create it in `server.ts`:

## Options
The `TypedRouter` class takes the following options.
### invalidRequestHandler
By default, if request validation fails, crosswalk returns a 400 status code and a descriptive
error. If you'd like to do something else, you may specify your own `invalidRequestHandler`. For
example, you might like to log the error or omit validation details from the response in prod.
This is the default implementation (`crosswalk.defaultInvalidRequestHandler`):
```ts
new TypedRouter<API>(app, apiSchema, {
handleInvalidRequest({response, payload, ajv, errors}) {
response.status(400).json({
error: ajv.errorsText(errors),
errors,
invalidRequest: payload,
});
}
});
```
## Questions

@@ -283,2 +311,39 @@

**Should I set `noExtraProps` with `typescript-json-schema`?**
There are many options you can set when you run [`typescript-json-schema`][tsjs]. You should think
carefully about these as they have an impact on the runtime behavior of your code.
You should set `strictNullChecks` to whatever it's set to in your `tsconfig.json`. (It should
really be set to `true`!). This ensures that the runtime checking and static type checking are in
agreement.
The `noExtraProps` option is more interesting. TypeScript uses a "structural" or "duck" typing
system. This means that an object may have the declared properties in its type, _but it could have
others, too_!
```ts
interface Hero {
heroName: string;
}
const superman = {
heroName: 'Superman',
alterEgo: 'Clark Kent',
};
declare function getHeroDetails(hero: Hero): string;
getHeroDetails(superman); // ok!
```
This is simply the way that TypeScript works, and so it must be the way that crosswalk statically
enforces your request types. If you're comfortable with this behavior, leave `noExtraProps` off.
If you _do_ specify `noExtraProps`, additional properties on a request will result in the request
being rejected at runtime with a 400 HTTP response. This has pros and cons. The pros are that it
will catch more user errors (e.g. misspelling an optional property name) and allows the server to
be more confident about the shape of its input (`Object.keys` won't produce surprises). The con
is that your runtime behavior is divergent from the static type checking, so client code that
passes the type checker might produce failing requests at runtime. Until TypeScript gets
["exact" types][exact], it will not be able to fully model `noExtraProps` statically.
**What's with the name?**

@@ -313,28 +378,2 @@

## TODO
- [ ] Options for request logging
- [ ] Add an option for more express-like callbacks (w/ only request, response)
- [ ] Support fancier paths
- [ ] Set up:
- [ ] eslint
- [x] prettier
- [x] CI
- [x] Add helper methods for all HTTP verbs
- [x] Look into cleaning up generics
- [x] Set up better type tests
- [x] Narrow types of request.params, request.body in handlers
- [x] Write unit tests
- [x] Decide on a name
- [x] Figure out how to handle `@types` deps (peer deps?)
- [x] Decide on a parameter ordering for methods
- [x] Should TypedRouter be a class ~or a function~?
- [x] Plug into cityci
- [x] Add a check that all endpoints are implemented
- [x] Make a demo project, maybe TODO or based on GraphQL demo
- [x] Add helpers for constructing URLs
- [x] Look into generating API docs, e.g. w/ Swagger
- [x] Make the runtime validation part optional
- [x] Plug in TS 4.1 template literal types
[tsjs]: https://github.com/YousefED/typescript-json-schema

@@ -350,1 +389,2 @@ [ajv]: https://ajv.js.org/

[swl]: https://sidewalklabs.com/
[exact]: https://github.com/microsoft/TypeScript/issues/12936
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc