Skip to content

Commit

Permalink
feat: add filter by method
Browse files Browse the repository at this point in the history
  • Loading branch information
rawnly committed Mar 31, 2024
1 parent d284a16 commit 78ee667
Show file tree
Hide file tree
Showing 8 changed files with 225 additions and 22 deletions.
56 changes: 56 additions & 0 deletions docs/src/pages/examples/filter-by-request-method.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
### Example - Filter by request method

This examples shows you how you can filter by request method

```ts filename="middleware.ts" copy showLineNumbers
import { NextResponse, NextRequest } from "next/server";
import { handlePaths, NextREquestWithParams } from "next-wayfinder";
import { createMiddlewareSupabaseClient } from "@supabase/auth-helpers-nextjs";

const getSession = async (req: NextRequestWithParams) => {
const res = NextResponse.next();
const supabase = createMiddlewareSupabaseClient({ req, res });

const {
data: { session },
error,
} = await supabase.auth.getSession();

if (error) {
console.error(`Auth Error: `, error);
return null;
}

return session;
};

export default handlePaths(
[
{
// do not check auth on GET requests
path: "/events/:id",
method: "GET",
handler: (_, res) => res,
},
{
// auth guard on PATCH
path: "/events/:id",
method: "PATCH",
pre: req => req.ctx?.session ? true : { redirectTo: "/auth/sign-in" },
handler: (req, res) => {
console.log("User authenticated: ", req.ctx?.session);
// do your stuff here
return res;
},
},
],
{
// this injects `session` property into the request object
context: async req => {
const session = await getSession(req);

return { session };
},
}
);
```
1 change: 1 addition & 0 deletions docs/src/pages/examples/supabase-authentication.mdx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Supabase Authentication

Below an example on how to integrate supabase authentication.

```ts filename="middleware.ts" copy showLineNumbers
Expand Down
7 changes: 7 additions & 0 deletions docs/src/pages/guide/middlewares.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ These properties are available in all the middlewares types
```ts
interface BaseMiddleware {
guard?: (params: UrlParams) => boolean;
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS';
pre?: (request: NextRequestWithParams<T>) => MaybePromise<
| boolean
| {
Expand All @@ -26,6 +27,9 @@ interface BaseMiddleware {
| ---- | ---- |
| `guard` | A function that checks if the given path arguments are valid. If `false` is returned then the middleware is skipped. |
| `pre` | This is executed before the middleware. It can be useful to check authentication and separate the logic.
| `method` | Filter the middleware by the HTTP method. If the method does not match the middleware is skipped. |

> **⚠️ WARNING**: The `method` property is not available on the `HostnameMiddleware` type.

## Path Middleware
Expand All @@ -35,6 +39,7 @@ This is the standard middleware that is executed when the path is matched.
interface PathMiddleware<T> {
handler: NextMiddlewareWithParams<T> | Middleware<T>[];
path: PathMatcher;
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS';
guard?: (params: UrlParams) => boolean;
pre?: (request: NextRequestWithParams<T>) => MaybePromise<
| boolean
Expand Down Expand Up @@ -73,6 +78,7 @@ If you need a more advanced redirect logic you can use the [`PathMiddleware`](#p
```ts
interface RedirectMiddleware<T> {
path: PathMatcher;
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS';
guard?: (params: UrlParams) => boolean;
redirectTo: string | ((req: NextRequestWithParams<T>) => string);
includeOrigin?: string | boolean;
Expand Down Expand Up @@ -101,6 +107,7 @@ This middleware is executed when the path is matched and it rewrites the path to
```ts
interface RewriteMiddleware<T> {
path: PathMatcher;
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS';
guard?: (params: UrlParams) => boolean;
rewriteTo: string | ((req: NextRequestWithParams<T>) => string);
}
Expand Down
57 changes: 57 additions & 0 deletions packages/next-wayfinder/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,63 @@ export default handlePaths(
);
```

### Example - Filter by request method
This examples shows you how you can filter by request method

```ts
// middleware.ts
import { NextResponse, NextRequest } from "next/server";
import { handlePaths, NextREquestWithParams } from "next-wayfinder";
import { createMiddlewareSupabaseClient } from "@supabase/auth-helpers-nextjs";

const getSession = async (req: NextRequestWithParams) => {
const res = NextResponse.next();
const supabase = createMiddlewareSupabaseClient({ req, res });

const {
data: { session },
error,
} = await supabase.auth.getSession();

if (error) {
console.error(`Auth Error: `, error);
return null;
}

return session;
};

export default handlePaths(
[
{
// do not check auth on GET requests
path: "/events/:id",
method: "GET",
handler: (_, res) => res,
},
{
// auth guard on PATCH
path: "/events/:id",
method: "PATCH",
pre: req => req.ctx?.session ? true : { redirectTo: "/auth/sign-in" },
handler: (req, res) => {
console.log("User authenticated: ", req.ctx?.session);
// do your stuff here
return res;
},
},
],
{
// this injects `session` property into the request object
context: async req => {
const session = await getSession(req);

return { session };
},
}
);
```

### Options

You can pass several options to configure your middleware
Expand Down
79 changes: 70 additions & 9 deletions packages/next-wayfinder/__tests__/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import { NextRequest, NextResponse } from "next/server";
import { test, expect } from "vitest";
import { test, expect, vi } from "vitest";

import { Middleware } from "../src/types";
import { Middleware, NextRequestWithParams } from "../src/types";
import {
addParams,
findMiddleware,
getParams,
applyContext,
type FindOptions
} from "../src/utils";

const queryForDomain = { hostname: "app.acme.org", path: "/" };
const queryForPath = { hostname: "", path: "/dashboard/it" };
const queryForDomain: FindOptions = { hostname: "app.acme.org", path: "/", method: "GET" };
const queryForPath: FindOptions = { hostname: "", path: "/dashboard/it", method:"GET" };

const middlewares: Middleware<unknown>[] = [
{
Expand Down Expand Up @@ -41,12 +42,72 @@ const middlewares: Middleware<unknown>[] = [
redirectTo: "/dashboard/events/:slug",
includeOrigin: true,
},

];

test('should find the middleware with the right method', () => {
const post = vi.fn();
const get = vi.fn();

const extra: Middleware<unknown>[] = [
{
path: "/api/events/create",
handler: post,
method: "POST"
},
{
path: "/api/events/create",
handler: get,
method: "GET"
}
]

const post_middleware = findMiddleware([...extra, ...middlewares], {
...queryForPath,
path: '/api/events/create',
method: "POST",
})

expect(post_middleware).toBeDefined();
expect(post_middleware).toHaveProperty("method");
if (!post_middleware || Middleware.isHostname(post_middleware)) return;
expect(post_middleware.method).toBe("POST");

if (Middleware.isPath(post_middleware)) {
expect(post_middleware.handler).toBeInstanceOf(Function)

if (Array.isArray(post_middleware.handler)) return
const request = new NextRequest(new URL("https://google.it")) as NextRequestWithParams<unknown>
post_middleware.handler(request, {} as any, {} as any)
expect(post).toHaveBeenCalledWith(request, {}, {})
}

const get_middleware = findMiddleware([...extra, ...middlewares], {
...queryForPath,
path: '/api/events/create',
method: "GET",
})

expect(get_middleware).toBeDefined();
expect(get_middleware).toHaveProperty("method");
if (!get_middleware || Middleware.isHostname(get_middleware)) return;
expect(get_middleware.method).toBe("GET");

if (Middleware.isPath(get_middleware)) {
expect(get_middleware.handler).toBeInstanceOf(Function)

if (Array.isArray(get_middleware.handler)) return
const request = new NextRequest(new URL("https://google.it")) as NextRequestWithParams<unknown>
get_middleware.handler(request, {} as any, {} as any)
expect(get).toHaveBeenCalledWith(request, {}, {})
}
})

test("should use the fallback middleware", () => {
const middleware = findMiddleware(middlewares, {
...queryForPath,
path: "/abc",
method: 'GET'
});

expect(middleware).toBeDefined();
Expand All @@ -64,7 +125,7 @@ test("should find the middleware with array", () => {
handler: () => null,
},
],
{ ...queryForPath, path: "/login" }
{ ...queryForPath, path: "/login", method: "GET" }
);

expect(middleware).toBeTruthy();
Expand All @@ -76,7 +137,7 @@ test("should find the middleware with string", () => {
expect(middleware).not.toBeUndefined();
expect(middleware).toHaveProperty("path");

if (!middleware?.path) return;
if (!middleware || Middleware.isHostname(middleware)) return;

expect(middleware.guard?.({ lang: "it" })).toBe(true);

Expand All @@ -93,7 +154,7 @@ test("should retrive the params", () => {
expect(middleware).not.toBeUndefined();
expect(middleware).toHaveProperty("path");

if (!middleware?.path) return;
if (!middleware || Middleware.isHostname(middleware)) return;

const params = getParams(middleware.path, "/dashboard/it");

Expand All @@ -107,7 +168,7 @@ test("should add the params", () => {
expect(middleware).not.toBeUndefined();
expect(middleware).toHaveProperty("path");

if (!middleware?.path) return;
if (!middleware || Middleware.isHostname(middleware)) return;

const request = new NextRequest(new URL("http://localhost:3000"));
const requestWithParams = addParams(
Expand All @@ -128,7 +189,7 @@ test("shuld inject", () => {
expect(middleware).not.toBeUndefined();
expect(middleware).toHaveProperty("path");

if (!middleware?.path) return;
if (!middleware || Middleware.isHostname(middleware)) return;

const request = new NextRequest(new URL("http://localhost:3000"));
const injectedRequest = applyContext(request, { ok: true });
Expand Down
2 changes: 2 additions & 0 deletions packages/next-wayfinder/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { match } from "ts-pattern";

import {
BeforeAllMiddleware,
HTTPMethod,
Middleware,
NextRequestWithParams,
RequestInjector,
Expand Down Expand Up @@ -120,6 +121,7 @@ export function handlePaths<T>(
const middleware = findMiddleware(middlewares, {
path: path,
hostname,
method: req.method.toUpperCase() as HTTPMethod,
});

if (options?.debug) {
Expand Down
21 changes: 13 additions & 8 deletions packages/next-wayfinder/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,19 +45,21 @@ export interface RequestInjector<T> {
(request: NextRequestWithParams<T>): MaybePromise<T>;
}

export type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS';


export type Middleware<T> =
| PathMiddleware<T>
| HostnameMiddleware<T>
| RedirectMiddleware<T>
| RewriteMiddleware<T>;
| PathMiddleware<T>
| RedirectMiddleware<T>
| RewriteMiddleware<T>
| HostnameMiddleware<T>;

export type HostnameCheck = string | RegExp | ((hostname: string) => boolean);

export interface HostnameMiddleware<T> {
handler: NextMiddlewareWithParams<T> | Middleware<T>[];
hostname: HostnameCheck | HostnameCheck[];
beforeAll?: BeforeAllMiddleware;
guard?: (params: UrlParams) => boolean;
pre?: (request: NextRequestWithParams<T>) => MaybePromise<
| boolean
| {
Expand All @@ -68,9 +70,10 @@ export interface HostnameMiddleware<T> {
}

export interface PathMiddleware<T> {
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS';
guard?: (params: UrlParams) => boolean;
handler: NextMiddlewareWithParams<T> | Middleware<T>[];
path: PathMatcher;
guard?: (params: UrlParams) => boolean;
pre?: (request: NextRequestWithParams<T>) => MaybePromise<
| boolean
| {
Expand All @@ -81,15 +84,17 @@ export interface PathMiddleware<T> {
}

export interface RedirectMiddleware<T> {
path: PathMatcher;
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS';
guard?: (params: UrlParams) => boolean;
path: PathMatcher;
redirectTo: string | ((req: NextRequestWithParams<T>) => string);
includeOrigin?: string | boolean;
}

export interface RewriteMiddleware<T> {
path: PathMatcher;
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS';
guard?: (params: UrlParams) => boolean;
path: PathMatcher;
rewriteTo: string | ((req: NextRequestWithParams<T>) => string);
}

Expand Down
Loading

0 comments on commit 78ee667

Please sign in to comment.