refactored context and router
This commit is contained in:
@ -3,13 +3,13 @@ import { type ExtractRouteParams } from "@lib/router.ts";
|
||||
import { fromNullableVal, none, Option, some } from "@shared/utils/option.ts";
|
||||
import { deleteCookie, getCookies, setCookie } from "@std/http/cookie";
|
||||
import { type Cookie } from "@std/http/cookie";
|
||||
import { getMessageFromError } from "@shared/utils/result.ts";
|
||||
import { getMessageFromError, ok } from "@shared/utils/result.ts";
|
||||
import {
|
||||
InferSchemaType,
|
||||
Schema,
|
||||
SchemaValidationError,
|
||||
} from "@shared/utils/validator.ts";
|
||||
import { ResultAsync } from "@shared/utils/resultasync.ts";
|
||||
import { okAsync, ResultAsync } from "@shared/utils/resultasync.ts";
|
||||
import log from "@shared/utils/logger.ts";
|
||||
import { FailedToParseRequestAsJSON } from "@src/lib/errors.ts";
|
||||
|
||||
@ -25,63 +25,79 @@ const SECURITY_HEADERS: Headers = new Headers({
|
||||
//"Content-Security-Policy":
|
||||
// "default-src 'self'; script-src 'self' 'unsafe-inline'",
|
||||
});
|
||||
const HTML_CONTENT_TYPE: [string, string] = [
|
||||
"Content-Type",
|
||||
"text/html; charset=UTF-8",
|
||||
];
|
||||
const JSON_CONTENT_TYPE: [string, string] = [
|
||||
"Content-Type",
|
||||
"application/json; charset=utf-8",
|
||||
];
|
||||
|
||||
const HTML_CONTENT_TYPE: string = "text/html; charset=UTF-8";
|
||||
const JSON_CONTENT_TYPE: string = "application/json; charset=utf-8";
|
||||
|
||||
function mergeHeaders(...headers: Headers[]): Headers {
|
||||
const mergedHeaders = new Headers();
|
||||
for (const _headers of headers) {
|
||||
for (const [key, value] of _headers.entries()) {
|
||||
mergedHeaders.set(key, value);
|
||||
const merged = new Headers();
|
||||
for (const hdr of headers) {
|
||||
hdr.forEach((value, key) => merged.set(key, value));
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
return mergedHeaders;
|
||||
|
||||
export type ContextWithSchema<
|
||||
C extends Context<string, any, any>,
|
||||
ReqSchema extends Schema<any>,
|
||||
ResSchema extends Schema<any>,
|
||||
> = C extends Context<infer S, any, any> ? Context<S, ReqSchema, ResSchema> & {
|
||||
schema: { req: ReqSchema; res: ResSchema };
|
||||
}
|
||||
: never;
|
||||
|
||||
export class Context<
|
||||
S extends string = string,
|
||||
ReqSchema extends Schema<any> | never = never,
|
||||
ResSchema extends Schema<any> | never = never,
|
||||
ReqSchema extends Schema<any> = Schema<unknown>,
|
||||
ResSchema extends Schema<any> = Schema<unknown>,
|
||||
> {
|
||||
private _url?: URL;
|
||||
private _hostname?: string;
|
||||
private _port?: number;
|
||||
private _cookies?: Record<string, string>;
|
||||
private _responseHeaders: Headers = new Headers();
|
||||
public res: Response = new Response();
|
||||
|
||||
public res = new Response();
|
||||
|
||||
constructor(
|
||||
public readonly req: Request,
|
||||
public readonly info: Deno.ServeHandlerInfo<Deno.Addr>,
|
||||
public readonly params: Params<ExtractRouteParams<S>>,
|
||||
public schema: [ReqSchema, ResSchema] extends [never, never] ? never
|
||||
: {
|
||||
req: ReqSchema;
|
||||
res: ResSchema;
|
||||
},
|
||||
) {}
|
||||
|
||||
public schema?: { req: ReqSchema; res: ResSchema };
|
||||
|
||||
public withSchema<
|
||||
Req extends Schema<any>,
|
||||
Res extends Schema<any>,
|
||||
>(
|
||||
schema: { req: Req; res: Res },
|
||||
): Context<S, Req, Res> & { schema: { req: Req; res: Res } } {
|
||||
const ctx = new Context<S, Req, Res>(this.req, this.info, this.params);
|
||||
ctx._url = this._url;
|
||||
ctx._hostname = this._hostname;
|
||||
ctx._port = this._port;
|
||||
ctx._cookies = this._cookies;
|
||||
ctx.res = this.res;
|
||||
ctx.schema = schema;
|
||||
return ctx as Context<S, Req, Res> & { schema: { req: Req; res: Res } };
|
||||
}
|
||||
|
||||
public parseBody(): ResultAsync<
|
||||
InferSchemaType<ReqSchema>,
|
||||
SchemaValidationError | FailedToParseRequestAsJSON
|
||||
> {
|
||||
if (!this.schema || !this.schema.req) {
|
||||
log.error("No schema provided!");
|
||||
Deno.exit(1);
|
||||
}
|
||||
|
||||
return ResultAsync
|
||||
.fromPromise(
|
||||
this.req.json(),
|
||||
(e) => new FailedToParseRequestAsJSON(getMessageFromError(e)),
|
||||
)
|
||||
.andThen((data: unknown) => this.schema.req.parse(data));
|
||||
.andThen((data: unknown) => {
|
||||
if (!this.schema) {
|
||||
return ok(data);
|
||||
}
|
||||
|
||||
return this.schema?.req.parse(data);
|
||||
});
|
||||
}
|
||||
|
||||
get url(): URL {
|
||||
@ -93,29 +109,19 @@ export class Context<
|
||||
}
|
||||
|
||||
get preferredType(): Option<"json" | "html"> {
|
||||
const headers = new Headers(this.req.headers);
|
||||
return fromNullableVal(headers.get("accept")).andThen(
|
||||
(types_header) => {
|
||||
const types = types_header.split(";")[0].trim().split(",");
|
||||
|
||||
for (const type of types) {
|
||||
if (type === "text/html") {
|
||||
return some("html");
|
||||
}
|
||||
if (type === "application/json") {
|
||||
return some("json");
|
||||
}
|
||||
}
|
||||
const accept = this.req.headers.get("accept");
|
||||
if (!accept) return none;
|
||||
const types = accept
|
||||
.split(",")
|
||||
.map((t) => t.split(";")[0].trim());
|
||||
if (types.includes("text/html")) return some("html");
|
||||
if (types.includes("application/json")) return some("json");
|
||||
return none;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
get hostname(): Option<string> {
|
||||
if (this._hostname) return some(this._hostname);
|
||||
|
||||
const remoteAddr = this.info.remoteAddr;
|
||||
|
||||
if (remoteAddr.transport === "tcp" || remoteAddr.transport === "udp") {
|
||||
this._hostname = remoteAddr.hostname;
|
||||
return some(remoteAddr.hostname);
|
||||
@ -125,9 +131,7 @@ export class Context<
|
||||
|
||||
get port(): Option<number> {
|
||||
if (this._port) return some(this._port);
|
||||
|
||||
const remoteAddr = this.info.remoteAddr;
|
||||
|
||||
if (remoteAddr.transport === "tcp" || remoteAddr.transport === "udp") {
|
||||
this._port = remoteAddr.port;
|
||||
return some(remoteAddr.port);
|
||||
@ -135,19 +139,27 @@ export class Context<
|
||||
return none;
|
||||
}
|
||||
|
||||
private buildHeaders(
|
||||
initHeaders?: HeadersInit,
|
||||
contentType?: string,
|
||||
): Headers {
|
||||
const merged = mergeHeaders(
|
||||
SECURITY_HEADERS,
|
||||
this.res.headers,
|
||||
new Headers(initHeaders),
|
||||
);
|
||||
if (contentType) merged.set("Content-Type", contentType);
|
||||
return merged;
|
||||
}
|
||||
|
||||
public json(
|
||||
body?: ResSchema extends Schema<infer T> ? T : object | string,
|
||||
init: ResponseInit = {},
|
||||
): Response {
|
||||
const headers = mergeHeaders(
|
||||
SECURITY_HEADERS,
|
||||
this._responseHeaders,
|
||||
new Headers(init.headers),
|
||||
);
|
||||
headers.set(...JSON_CONTENT_TYPE);
|
||||
let status = init.status || 200;
|
||||
|
||||
const headers = this.buildHeaders(init.headers, JSON_CONTENT_TYPE);
|
||||
let status = init.status ?? 200;
|
||||
let responseBody: BodyInit | null = null;
|
||||
|
||||
if (typeof body === "string") {
|
||||
responseBody = body;
|
||||
} else if (body !== undefined) {
|
||||
@ -162,82 +174,67 @@ export class Context<
|
||||
}
|
||||
}
|
||||
|
||||
return new Response(responseBody, {
|
||||
this.res = new Response(responseBody, {
|
||||
status,
|
||||
headers,
|
||||
});
|
||||
return this.res;
|
||||
}
|
||||
|
||||
public json400(body?: object | string, init: ResponseInit = {}) {
|
||||
init.status = 400;
|
||||
return this.json(body, init);
|
||||
public json400(
|
||||
body?: ResSchema extends Schema<infer T> ? T : object | string,
|
||||
init: ResponseInit = {},
|
||||
): Response {
|
||||
return this.json(body, { ...init, status: 400 });
|
||||
}
|
||||
|
||||
public json500(body?: object | string, init: ResponseInit = {}) {
|
||||
init.status = 400;
|
||||
return this.json(body, init);
|
||||
public json500(
|
||||
body?: ResSchema extends Schema<infer T> ? T : object | string,
|
||||
init: ResponseInit = {},
|
||||
) {
|
||||
return this.json(body, { ...init, status: 500 });
|
||||
}
|
||||
|
||||
public html(body?: BodyInit | null, init: ResponseInit = {}): Response {
|
||||
const headers = mergeHeaders(
|
||||
SECURITY_HEADERS,
|
||||
this._responseHeaders,
|
||||
new Headers(init.headers),
|
||||
);
|
||||
headers.set(...HTML_CONTENT_TYPE);
|
||||
const headers = this.buildHeaders(init.headers, HTML_CONTENT_TYPE);
|
||||
const status = init.status ?? 200;
|
||||
|
||||
return new Response(body ?? null, {
|
||||
status,
|
||||
headers,
|
||||
});
|
||||
this.res = new Response(body ?? null, { status, headers });
|
||||
return this.res;
|
||||
}
|
||||
|
||||
public redirect(url: string, permanent = false): Response {
|
||||
const headers = mergeHeaders(
|
||||
this._responseHeaders,
|
||||
this.res.headers,
|
||||
new Headers({ location: url }),
|
||||
);
|
||||
|
||||
return new Response(null, {
|
||||
this.res = new Response(null, {
|
||||
status: permanent ? 301 : 302,
|
||||
headers,
|
||||
});
|
||||
return this.res;
|
||||
}
|
||||
|
||||
public cookies = (() => {
|
||||
const self = this;
|
||||
|
||||
public get cookies() {
|
||||
return {
|
||||
get(name: string): Option<string> {
|
||||
if (!self._cookies) {
|
||||
self._cookies = getCookies(self.req.headers);
|
||||
}
|
||||
|
||||
return fromNullableVal(self._cookies[name]);
|
||||
},
|
||||
|
||||
set(cookie: Cookie) {
|
||||
setCookie(self._responseHeaders, cookie);
|
||||
},
|
||||
|
||||
delete(name: string) {
|
||||
deleteCookie(self._responseHeaders, name);
|
||||
get: (name: string): Option<string> => {
|
||||
this._cookies ??= getCookies(this.req.headers);
|
||||
return fromNullableVal(this._cookies[name]);
|
||||
},
|
||||
set: (cookie: Cookie) => setCookie(this.res.headers, cookie),
|
||||
delete: (name: string) => deleteCookie(this.res.headers, name),
|
||||
};
|
||||
})();
|
||||
}
|
||||
|
||||
static setParams<S extends string>(
|
||||
ctx: Context<string>,
|
||||
params: Params<ExtractRouteParams<S>>,
|
||||
): Context<S> {
|
||||
const newCtx = new Context(ctx.req, ctx.info, params);
|
||||
const newCtx = new Context(ctx.req, ctx.info, params) as typeof ctx;
|
||||
|
||||
newCtx._url = ctx._url;
|
||||
newCtx._hostname = ctx._hostname;
|
||||
newCtx._port = ctx._port;
|
||||
newCtx._cookies = ctx._cookies;
|
||||
newCtx._responseHeaders = ctx._responseHeaders;
|
||||
newCtx.schema = ctx.schema;
|
||||
|
||||
return newCtx;
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import {
|
||||
Schema,
|
||||
SchemaValidationError,
|
||||
ValidationErrorDetail,
|
||||
z,
|
||||
} from "@shared/utils/validator.ts";
|
||||
|
||||
export class ErrorBase extends Error {
|
||||
|
||||
@ -8,9 +8,8 @@ type RequestHandler<
|
||||
S extends string,
|
||||
ReqSchema extends Schema<any> = never,
|
||||
ResSchema extends Schema<any> = never,
|
||||
> = (
|
||||
c: Context<S, ReqSchema, ResSchema>,
|
||||
) => Promise<Response> | Response;
|
||||
> = (c: Context<S, ReqSchema, ResSchema>) => Promise<Response> | Response;
|
||||
|
||||
type RequestHandlerWithSchema<S extends string> = {
|
||||
handler: RequestHandler<S>;
|
||||
schema?: {
|
||||
@ -37,20 +36,22 @@ const DEFAULT_NOT_FOUND_HANDLER = () => new Response("404 Not found");
|
||||
|
||||
class HttpRouter {
|
||||
public readonly routerTree = new RouterTree<MethodHandlers<any>>();
|
||||
pathPreprocessor?: (path: string) => string;
|
||||
public pathPreprocessor?: (path: string) => string;
|
||||
private middlewares: Middleware[] = [];
|
||||
defaultNotFoundHandler: RequestHandler<string> = DEFAULT_NOT_FOUND_HANDLER;
|
||||
public defaultNotFoundHandler: RequestHandler<string> =
|
||||
DEFAULT_NOT_FOUND_HANDLER;
|
||||
|
||||
setPathProcessor(processor: (path: string) => string) {
|
||||
public setPathProcessor(processor: (path: string) => string) {
|
||||
this.pathPreprocessor = processor;
|
||||
return this;
|
||||
}
|
||||
|
||||
use(mw: Middleware): HttpRouter {
|
||||
public use(mw: Middleware): this {
|
||||
this.middlewares.push(mw);
|
||||
return this;
|
||||
}
|
||||
|
||||
add<S extends string>(
|
||||
public add<S extends string>(
|
||||
path: S,
|
||||
method: string,
|
||||
handler: RequestHandler<S>,
|
||||
@ -59,7 +60,7 @@ class HttpRouter {
|
||||
req: Schema<any>;
|
||||
},
|
||||
): HttpRouter;
|
||||
add<S extends string>(
|
||||
public add<S extends string>(
|
||||
path: S[],
|
||||
method: string,
|
||||
handler: RequestHandler<string>,
|
||||
@ -68,7 +69,7 @@ class HttpRouter {
|
||||
req: Schema<any>;
|
||||
},
|
||||
): HttpRouter;
|
||||
add(
|
||||
public add(
|
||||
path: string | string[],
|
||||
method: string,
|
||||
handler: RequestHandler<string>,
|
||||
@ -95,31 +96,43 @@ class HttpRouter {
|
||||
return this;
|
||||
}
|
||||
|
||||
get<S extends string>(path: S, handler: RequestHandler<S>): HttpRouter;
|
||||
get<S extends string>(
|
||||
public get<S extends string>(
|
||||
path: S,
|
||||
handler: RequestHandler<S>,
|
||||
): HttpRouter;
|
||||
public get<S extends string>(
|
||||
path: S[],
|
||||
handler: RequestHandler<string>,
|
||||
): HttpRouter;
|
||||
get(path: string | string[], handler: RequestHandler<string>): HttpRouter {
|
||||
public get(
|
||||
path: string | string[],
|
||||
handler: RequestHandler<string>,
|
||||
): HttpRouter {
|
||||
if (Array.isArray(path)) {
|
||||
return this.add(path, "GET", handler);
|
||||
}
|
||||
return this.add(path, "GET", handler);
|
||||
}
|
||||
|
||||
post<S extends string>(path: S, handler: RequestHandler<S>): HttpRouter;
|
||||
post<S extends string>(
|
||||
public post<S extends string>(
|
||||
path: S,
|
||||
handler: RequestHandler<S>,
|
||||
): HttpRouter;
|
||||
public post<S extends string>(
|
||||
path: string[],
|
||||
handler: RequestHandler<string>,
|
||||
): HttpRouter;
|
||||
post(path: string | string[], handler: RequestHandler<string>): HttpRouter {
|
||||
public post(
|
||||
path: string | string[],
|
||||
handler: RequestHandler<string>,
|
||||
): HttpRouter {
|
||||
if (Array.isArray(path)) {
|
||||
return this.add(path, "POST", handler);
|
||||
}
|
||||
return this.add(path, "POST", handler);
|
||||
}
|
||||
|
||||
api<
|
||||
public api<
|
||||
Path extends string,
|
||||
ReqSchema extends Schema<any>,
|
||||
ResSchema extends Schema<any>,
|
||||
@ -134,31 +147,23 @@ class HttpRouter {
|
||||
req: Request,
|
||||
connInfo: Deno.ServeHandlerInfo<Deno.Addr>,
|
||||
): Promise<Response> {
|
||||
const c = new Context(req, connInfo, {});
|
||||
|
||||
let ctx = new Context(req, connInfo, {});
|
||||
let routeParams: Record<string, string> = {};
|
||||
const path = this.pathPreprocessor
|
||||
? this.pathPreprocessor(c.path)
|
||||
: c.path;
|
||||
|
||||
let params: Record<string, string> = {};
|
||||
? this.pathPreprocessor(ctx.path)
|
||||
: ctx.path;
|
||||
|
||||
const handler = this.routerTree
|
||||
.find(path)
|
||||
.andThen((routeMatch) => {
|
||||
const { value: methodToHandler, params: paramsMatched } =
|
||||
routeMatch;
|
||||
|
||||
params = paramsMatched;
|
||||
|
||||
const handlerAndSchema = methodToHandler[req.method];
|
||||
|
||||
if (!handlerAndSchema) {
|
||||
return none;
|
||||
.andThen((match) => {
|
||||
const { value: methodHandler, params: params } = match;
|
||||
routeParams = params;
|
||||
const route = methodHandler[req.method];
|
||||
if (!route) return none;
|
||||
if (route.schema) {
|
||||
ctx = ctx.withSchema(route.schema);
|
||||
}
|
||||
|
||||
const handler = handlerAndSchema.handler;
|
||||
|
||||
c.schema = handlerAndSchema.schema;
|
||||
const handler = route.handler;
|
||||
|
||||
return some(handler);
|
||||
})
|
||||
@ -167,7 +172,7 @@ class HttpRouter {
|
||||
const res = (await this.executeMiddlewareChain(
|
||||
this.middlewares,
|
||||
handler,
|
||||
Context.setParams(c, params),
|
||||
Context.setParams(ctx, routeParams),
|
||||
)).res;
|
||||
|
||||
return res;
|
||||
|
||||
@ -148,83 +148,31 @@ export class RouterTree<T> {
|
||||
|
||||
public add(path: string, handler: T): void {
|
||||
const segments = this.splitPath(path);
|
||||
const paramNames: string[] = this.extractParams(segments);
|
||||
let current: Node<T> = this.root;
|
||||
const paramNames: string[] = this.extractParamNames(segments);
|
||||
const node: Node<T> = this.traverseOrCreate(segments);
|
||||
|
||||
for (const segment of segments) {
|
||||
current = current
|
||||
.getChild(segment)
|
||||
.unwrapOrElse(() =>
|
||||
current.addChild(
|
||||
segment,
|
||||
this.wildcardSymbol,
|
||||
this.paramPrefix,
|
||||
)
|
||||
);
|
||||
|
||||
if (current.isWildcardNode()) {
|
||||
current.paramNames = paramNames;
|
||||
current.paramNames.push("restOfThePath");
|
||||
current.handler = some(handler);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
current.paramNames = paramNames;
|
||||
current.handler = some(handler);
|
||||
node.paramNames = node.isWildcardNode()
|
||||
? [...paramNames, "restOfThePath"]
|
||||
: paramNames;
|
||||
node.handler = some(handler);
|
||||
}
|
||||
|
||||
public find(path: string): Option<RouteMatch<T>> {
|
||||
const segments = this.splitPath(path);
|
||||
const paramValues: string[] = [];
|
||||
let current: Node<T> = this.root;
|
||||
let i = 0;
|
||||
|
||||
for (; i < segments.length; i++) {
|
||||
const segment = segments[i];
|
||||
if (current.isWildcardNode()) break;
|
||||
|
||||
const nextNode = current.getChild(segment).ifSome((child) => {
|
||||
if (child.isDynamicNode()) {
|
||||
paramValues.push(segment);
|
||||
}
|
||||
current = child;
|
||||
});
|
||||
|
||||
if (nextNode.isNone()) return none;
|
||||
}
|
||||
|
||||
if (current.isWildcardNode()) {
|
||||
const rest = segments.slice(i - 1);
|
||||
if (rest.length > 0) {
|
||||
paramValues.push(rest.join(this.pathSeparator));
|
||||
}
|
||||
}
|
||||
|
||||
return this.traverse(path).andThen(({ node, paramValues }) => {
|
||||
const params: Params = {};
|
||||
|
||||
for (let i = 0; i < paramValues.length; i++) {
|
||||
params[current.paramNames[i]] = paramValues[i];
|
||||
for (
|
||||
let i = 0;
|
||||
i < Math.min(paramValues.length, node.paramNames.length);
|
||||
i++
|
||||
) {
|
||||
params[node.paramNames[i]] = paramValues[i];
|
||||
}
|
||||
|
||||
return current.handler.map((value) => ({ value, params }));
|
||||
return node.handler.map((handler) => ({ value: handler, params }));
|
||||
});
|
||||
}
|
||||
|
||||
public getHandler(path: string): Option<T> {
|
||||
const segments = this.splitPath(path);
|
||||
let current: Node<T> = this.root;
|
||||
|
||||
for (const segment of segments) {
|
||||
if (current.isWildcardNode()) break;
|
||||
|
||||
const child = current.getChild(segment).ifSome((child) => {
|
||||
current = child;
|
||||
});
|
||||
|
||||
if (child.isNone()) return none;
|
||||
}
|
||||
|
||||
return current.handler;
|
||||
return this.traverse(path).andThen(({ node }) => node.handler);
|
||||
}
|
||||
|
||||
private traverseOrCreate(segments: string[]): Node<T> {
|
||||
@ -235,20 +183,49 @@ export class RouterTree<T> {
|
||||
node.addChild(segment, this.wildcardSymbol, this.paramPrefix)
|
||||
);
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
private traverse(
|
||||
path: string,
|
||||
): Option<{ node: Node<T>; paramValues: string[] }> {
|
||||
const segments = this.splitPath(path);
|
||||
const paramValues: string[] = [];
|
||||
let node: Node<T> = this.root;
|
||||
|
||||
for (let i = 0; i < segments.length; i++) {
|
||||
if (node.isWildcardNode()) {
|
||||
const remaining = segments.slice(i).join(this.pathSeparator);
|
||||
if (remaining) paramValues.push(remaining);
|
||||
return some({ node, paramValues });
|
||||
}
|
||||
|
||||
const childOpt = node.getChild(segments[i]);
|
||||
if (childOpt.isNone()) return none;
|
||||
|
||||
node = childOpt.unwrap();
|
||||
if (node.isDynamicNode()) {
|
||||
paramValues.push(segments[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return some({ node, paramValues });
|
||||
}
|
||||
|
||||
private splitPath(path: string): string[] {
|
||||
const trimmed = path.trim().replace(/^\/+/, "").replace(/\/+$/, "");
|
||||
return trimmed ? trimmed.split(this.pathSeparator) : [];
|
||||
return path
|
||||
.trim()
|
||||
.split(this.pathSeparator)
|
||||
.filter((segment) => segment.length > 0);
|
||||
}
|
||||
|
||||
public extractParams(segments: string[]): string[] {
|
||||
public extractParamNames(segments: string[]): string[] {
|
||||
return segments.filter((segment) =>
|
||||
segment.startsWith(this.paramPrefix)
|
||||
).map((segment) => this.stripParamPrefix(segment));
|
||||
).map((segment) => this.removeParamPrefix(segment));
|
||||
}
|
||||
|
||||
public stripParamPrefix(segment: string): string {
|
||||
public removeParamPrefix(segment: string): string {
|
||||
return segment.slice(this.paramPrefix.length);
|
||||
}
|
||||
}
|
||||
|
||||
BIN
server/test.db
BIN
server/test.db
Binary file not shown.
Reference in New Issue
Block a user