Skip to content
This repository has been archived by the owner on Nov 28, 2023. It is now read-only.

Commit

Permalink
Support optional parsers in mutations and queries (#26)
Browse files Browse the repository at this point in the history
  • Loading branch information
Schniz authored Sep 4, 2022
1 parent 6b8c239 commit f732e3c
Show file tree
Hide file tree
Showing 16 changed files with 361 additions and 90 deletions.
7 changes: 7 additions & 0 deletions .changeset/orange-yaks-wave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@next-fetch/core-plugin": patch
"@next-fetch/react-query": patch
"@next-fetch/swr": patch
---

Support optional input in `query` and `mutation`
4 changes: 2 additions & 2 deletions packages/@next-fetch/core-plugin/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,10 @@ export async function queryFetcher(url: string) {

export function buildUrlSearchParams(
handler: string,
object: Record<string, unknown>
object?: Record<string, unknown>
): URLSearchParams {
const sp = new URLSearchParams();
for (const [key, value] of Object.entries(object)) {
for (const [key, value] of Object.entries(object ?? {})) {
if (Array.isArray(value)) {
for (const v of value) {
sp.append(key, v);
Expand Down
6 changes: 4 additions & 2 deletions packages/@next-fetch/core-plugin/src/parseEndpointFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { simple } from "acorn-walk";

type ParsedQuery = {
parserCode: string;
callbackCode: string;
callbackCode?: string;
optionsCode?: string;
};
export type Queries = { [name: string]: ParsedQuery };
Expand Down Expand Up @@ -31,7 +31,9 @@ export function parseEndpointFile(content: string): {
"query" === declaration.init.callee.name ? queries : mutations;
bag[content.slice(name.start, name.end)] = {
parserCode: content.slice(parser.start, parser.end),
callbackCode: content.slice(callback.start, callback.end),
...(callback && {
callbackCode: content.slice(callback.start, callback.end),
}),
...(options && {
optionsCode: content.slice(options.start, options.end),
}),
Expand Down
69 changes: 49 additions & 20 deletions packages/@next-fetch/core-plugin/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,41 @@ export type HandlerCallback<Input, Output> = (
input: Input
) => Promise<Output>;

type Handler<Input, Output> = {
type NormalizedHandler<Input, Output> = {
parser: Parser<Input>;
callback: HandlerCallback<Input, Output>;
options: Partial<HookIntoResponse<Output>>;
};

type Handler<Input, Output> =
| NormalizedHandler<Input, Output>
| {
parser: HandlerCallback<Input, Output>;
callback: Partial<HookIntoResponse<Output>>;
options: undefined;
};

type Handlers = Map<string, Handler<unknown, unknown>>;

const API_CONTENT_TYPE = "application/json+api";

function normalize<Input, Output>(
handler: Handler<Input, Output>
): NormalizedHandler<Input, Output> {
// @ts-expect-error This fails for some reason
return typeof handler.parser === "function"
? {
parser: undefined,
callback: handler.parser,
options: handler.callback,
}
: {
parser: handler.parser,
callback: handler.callback,
options: handler.options,
};
}

export async function handleEdgeFunction({
queries,
mutations,
Expand All @@ -44,24 +69,26 @@ export async function handleEdgeFunction({
return new Response("unknown handler", { status: 400 });
}

const { parser, callback } = handler;
const { parser, callback, options } = normalize(handler);
let data: unknown;

try {
data = await parse(
parser,
bag === mutations
? await getRequestBody(request)
: fromSearchParamToObject(url.searchParams)
);
} catch (e: any) {
return new Response(e?.message || "Malformed input", { status: 400 });
if (parser) {
try {
data = await parse(
parser,
bag === mutations
? await getRequestBody(request)
: fromSearchParamToObject(url.searchParams)
);
} catch (e: any) {
return new Response(e?.message || "Malformed input", { status: 400 });
}
}

const response = await callback.call({ request }, data);

if (handler.options?.hookResponse && !forceJsonResponse) {
return handler.options.hookResponse.call({ request }, response);
if (options?.hookResponse && !forceJsonResponse) {
return options.hookResponse.call({ request }, response);
}

return NextResponse.json(response);
Expand Down Expand Up @@ -100,12 +127,14 @@ export async function handleNodejsFunction({
.send(getUnknownHandlerError(String(req.query.__handler), bag));
}

const { parser, callback } = handler;
const { parser, callback, options } = normalize(handler);
let data;
try {
data = await parse(parser, bag === mutations ? req.body : req.query);
} catch (e: any) {
return res.status(400).send(e?.message || "Malformed input");
if (parser) {
try {
data = await parse(parser, bag === mutations ? req.body : req.query);
} catch (e: any) {
return res.status(400).send(e?.message || "Malformed input");
}
}

/**
Expand All @@ -123,8 +152,8 @@ export async function handleNodejsFunction({

const response = await callback.call(context, data);

if (handler.options?.hookResponse && !forceJsonResponse) {
const manipulatedResponse = await handler.options.hookResponse.call(
if (options?.hookResponse && !forceJsonResponse) {
const manipulatedResponse = await options.hookResponse.call(
context,
response
);
Expand Down
25 changes: 20 additions & 5 deletions packages/@next-fetch/react-query/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,36 @@
import type { UseQueryResult, UseMutationResult } from "@tanstack/react-query";
import type { Parser } from "@next-fetch/core-plugin/parser";
import type { HandlerCallback } from "@next-fetch/core-plugin/server";
import type {
HandlerCallback,
HookIntoResponse,
} from "@next-fetch/core-plugin/server";
import type { NextConfig } from "next";
import type { HookMetadata } from "@next-fetch/core-plugin/client";
import { createPlugin } from "@next-fetch/core-plugin";

export function query<Output>(
callback: HandlerCallback<void, Output>,
options?: Partial<HookIntoResponse<Output>>
): () => UseQueryResult<Output>;
export function query<Input, Output>(
parser: Parser<Input>,
callback: HandlerCallback<Input, Output>
): (v: Input) => UseQueryResult<Output> {
callback: HandlerCallback<Input, Output>,
options?: Partial<HookIntoResponse<Output>>
): (v: Input) => UseQueryResult<Output>;
export function query(): unknown {
throw new Error("This code path should not be reached");
}

export function mutation<Output>(
callback: HandlerCallback<void, Output>,
options?: Partial<HookIntoResponse<Output>>
): () => UseMutationResult<Output, any, void> & { meta: HookMetadata };
export function mutation<Input, Output>(
parser: Parser<Input>,
callback: HandlerCallback<Input, Output>
): () => UseMutationResult<Output, any, Input> & { meta: HookMetadata } {
callback: HandlerCallback<Input, Output>,
options?: Partial<HookIntoResponse<Output>>
): () => UseMutationResult<Output, any, Input> & { meta: HookMetadata };
export function mutation(): unknown {
throw new Error("This code path should not be reached");
}

Expand Down
18 changes: 18 additions & 0 deletions packages/@next-fetch/swr/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,38 @@ import type {
import { createPlugin } from "@next-fetch/core-plugin";
import type { NextConfig } from "next";

export function query<Output>(
callback: HandlerCallback<void, Output>,
options?: Partial<HookIntoResponse<Output>>
): (() => SWRResponse<Output>) & { meta: HookMetadata };
export function query<Input, Output>(
parser: Parser<Input>,
callback: HandlerCallback<Input, Output>,
options?: Partial<HookIntoResponse<Output>>
): ((v: Input) => SWRResponse<Output>) & { meta: HookMetadata };
export function query<Input, Output>(
parser: unknown,
callback: unknown,
options?: unknown
): ((v: Input) => SWRResponse<Output>) & { meta: HookMetadata } {
throw new Error("This code path should not be reached");
}

export type MutationOptions<Output> = HookIntoResponse<Output>;

export function mutation<Output>(
callback: HandlerCallback<void, Output>,
options?: Partial<MutationOptions<Output>>
): () => SWRMutationResponse<Output, any, void> & { meta: HookMetadata };
export function mutation<Input, Output>(
parser: Parser<Input>,
callback: HandlerCallback<Input, Output>,
options?: Partial<MutationOptions<Output>>
): () => SWRMutationResponse<Output, any, Input> & { meta: HookMetadata };
export function mutation<Input, Output>(
parser: unknown,
callback: unknown,
options?: unknown
): (() => SWRMutationResponse<Output, any, Input> & { meta: HookMetadata }) & {
meta: HookMetadata;
} {
Expand Down
12 changes: 12 additions & 0 deletions packages/example-app/pages/api/edge.swr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,15 @@ export const useRuntimeInfoMutation = mutation(
},
}
);

export const useNoArgs = query(async () => `${EdgeRuntime}`);
export const useNoArgsMutation = query(
async function () {
return this.request.method;
},
{
hookResponse(data) {
return new Response(`response is: ${JSON.stringify(data)}`);
},
}
);
13 changes: 13 additions & 0 deletions packages/example-app/pages/api/people.swr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,19 @@ import { query, mutation } from "@next-fetch/swr";
import { userAgent } from "next/server";

export const useAllPeople = query(
async () => {
return `Many people are here!`;
},
{
hookResponse(text) {
return new Response(text, {
headers: { "x-direct-request": "true" },
});
},
}
);

export const usePerson = query(
z.object({ name: z.string() }),
async (user) => {
return `Hello, ${user.name} :D`;
Expand Down
47 changes: 47 additions & 0 deletions packages/example-app/pages/api/rq/edge.rq.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import z from "zod";
import { mutation, query } from "@next-fetch/react-query";
import { userAgent } from "next/server";

export const config = { runtime: "experimental-edge" };

export const useRuntimeInfo = query(
z.object({ name: z.string() }),
async ({ name }) => {
return `${name}, EdgeRuntime = ${EdgeRuntime}`;
},
{
hookResponse(text) {
return new Response(text, {
headers: { "x-direct-request": "true" },
});
},
}
);

export const useRuntimeInfoMutation = mutation(
z.object({ name: z.string() }),
async function ({ name }) {
return [
`runtime: ${EdgeRuntime}`,
`input: ${name}`,
`request browser: ${userAgent(this.request).browser?.name}`,
];
},
{
hookResponse(data) {
return new Response(`response is: ${JSON.stringify(data)}`);
},
}
);

export const useNoArgs = query(async () => `${EdgeRuntime}`);
export const useNoArgsMutation = query(
async function () {
return this.request.method;
},
{
hookResponse(data) {
return new Response(`response is: ${JSON.stringify(data)}`);
},
}
);
25 changes: 25 additions & 0 deletions packages/example-app/pages/api/rq/people.rq.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,29 @@ import { query, mutation } from "@next-fetch/react-query";
import { userAgent } from "next/server";

export const useAllPeople = query(
async () => {
return `Many people are here!`;
},
{
hookResponse(text) {
return new Response(text, {
headers: { "x-direct-request": "true" },
});
},
}
);

export const usePerson = query(
z.object({ name: z.string() }),
async (user) => {
return `Hello, ${user.name} :D`;
},
{
hookResponse(text) {
return new Response(text, {
headers: { "x-direct-request": "true" },
});
},
}
);

Expand All @@ -21,5 +41,10 @@ export const useListPeopleWith = mutation(
"Alice",
name.trim(),
];
},
{
hookResponse(data) {
return new Response(`response is: ${JSON.stringify(data)}`);
},
}
);
22 changes: 15 additions & 7 deletions packages/example-app/pages/edge.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
import { useRuntimeInfo } from "./api/edge.swr";
import { useRuntimeInfo, useNoArgs } from "./api/edge.swr";

export const config = { runtime: "experimental-edge" };

export default function Home(props: { runtime: string }) {
const result = useRuntimeInfo({ name: "gal" });
const runtimeInfo = useRuntimeInfo({ name: "gal" });
const noArgs = useNoArgs();

return (
<div>
<div id="result">
{result.data
? result.data
: result.error
? String(result.error)
<div id="runtimeInfo">
{runtimeInfo.data
? runtimeInfo.data
: runtimeInfo.error
? String(runtimeInfo.error)
: "loading..."}
</div>
<div id="noArgs">
{noArgs.data
? noArgs.data
: noArgs.error
? String(noArgs.error)
: "loading..."}
</div>
<p>Rendered on {props.runtime}</p>
Expand Down
Loading

2 comments on commit f732e3c

@vercel
Copy link

@vercel vercel bot commented on f732e3c Sep 4, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

next-fetch – ./packages/docs

next-fetch-vercel-labs.vercel.app
next-fetch-git-main-vercel-labs.vercel.app
next-fetch-pi.vercel.app

@vercel
Copy link

@vercel vercel bot commented on f732e3c Sep 4, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

next-swr-endpoints-example-app – ./packages/example-app

next-swr-endpoints-example-app.vercel.app
next-swr-endpoints-example-app-vercel-labs.vercel.app
next-swr-endpoints-example-app-git-main-vercel-labs.vercel.app

Please sign in to comment.