Skip to content

Commit

Permalink
Add GET support to RPC specification (#3891)
Browse files Browse the repository at this point in the history
  • Loading branch information
siddhsuresh authored Oct 29, 2022
1 parent 0ebdf3b commit ceb7db2
Show file tree
Hide file tree
Showing 12 changed files with 489 additions and 412 deletions.
7 changes: 7 additions & 0 deletions .changeset/itchy-cups-double.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"blitz": patch
"@blitzjs/rpc": patch
---

Add an opt-in GET request support to RPC specification by exporting a `config` object that has the `httpMethod` property.
from `query` files.
4 changes: 4 additions & 0 deletions apps/toolkit-app/app/users/queries/getCurrentUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,7 @@ export default async function getCurrentUser(_ = null, { session }: Ctx) {

return user
}

export const config = {
httpMethod: "GET",
}
16 changes: 16 additions & 0 deletions integration-tests/rpc/app/queries/getBasicWithGET.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
if (typeof window !== "undefined") {
throw new Error("This should not be loaded on the client")
}

export default async function getBasicWithGET() {
if (typeof window !== "undefined") {
throw new Error("This should not be loaded on the client")
}

global.basic ??= "basic-result"
return global.basic
}

export const config = {
httpMethod: "GET",
}
71 changes: 61 additions & 10 deletions integration-tests/rpc/test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,10 @@ import {
nextBuild,
nextStart,
nextExport,
getPageFileFromBuildManifest,
getPageFileFromPagesManifest,
} from "../../utils/next-test-utils"

// jest.setTimeout(1000 * 60 * 2)
const appDir = join(__dirname, "../")
const nextConfig = join(appDir, "next.config.js")
let appPort
let mode
let app
Expand All @@ -34,26 +31,80 @@ function runTests(dev = false) {
)

it(
"returns 404 for GET",
"requires params",
async () => {
const res = await fetchViaHTTP(appPort, "/api/rpc/getBasic", null, {
method: "GET",
method: "POST",
headers: {"Content-Type": "application/json; charset=utf-8"},
})
const json = await res.json()
expect(res.status).toEqual(400)
expect(json.error.message).toBe("Request body is missing the `params` key")
},
5000 * 60 * 2,
)

it(
"GET - returns 200 only when enabled",
async () => {
const res = await fetchViaHTTP(
appPort,
"/api/rpc/getBasicWithGET?params=%7B%7D&meta=%7B%7D",
null,
{
method: "GET",
},
)
expect(res.status).toEqual(200)
},
5000 * 60 * 2,
)

it(
"GET - returns 404 otherwise",
async () => {
const res = await fetchViaHTTP(
appPort,
"/api/rpc/getBasic?params=%7B%7D&meta=%7B%7D",
null,
{
method: "GET",
},
)
expect(res.status).toEqual(404)
},
5000 * 60 * 2,
)

it(
"requires params",
"query works - GET",
async () => {
const res = await fetchViaHTTP(appPort, "/api/rpc/getBasic", null, {
method: "POST",
headers: {"Content-Type": "application/json; charset=utf-8"},
const res = await fetchViaHTTP(
appPort,
"/api/rpc/getBasicWithGET?params=%7B%7D&meta=%7B%7D",
null,
{
method: "GET",
},
)
const json = await res.json()
expect(json).toEqual({result: "basic-result", error: null, meta: {}})
expect(res.status).toEqual(200)
},
5000 * 60 * 2,
)

it(
"requires params - GET",
async () => {
const res = await fetchViaHTTP(appPort, "/api/rpc/getBasicWithGET", null, {
method: "GET",
})
const json = await res.json()
expect(res.status).toEqual(400)
expect(json.error.message).toBe("Request body is missing the `params` key")
expect(json.error.message).toBe(
"Request query is missing the required `params` and `meta` keys",
)
},
5000 * 60 * 2,
)
Expand Down
1 change: 1 addition & 0 deletions packages/blitz-rpc/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
],
"dependencies": {
"@blitzjs/auth": "2.0.0-beta.15",
"@swc/core": "1.3.7",
"@tanstack/react-query": "4.0.10",
"b64-lite": "1.4.0",
"bad-behavior": "1.0.1",
Expand Down
29 changes: 20 additions & 9 deletions packages/blitz-rpc/src/data-client/rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {deserialize, serialize} from "superjson"
import {SuperJSONResult} from "superjson/dist/types"
import {CSRFTokenMismatchError, isServer} from "blitz"
import {getQueryKeyFromUrlAndParams, getQueryClient} from "./react-query-utils"
import {stringify} from "superjson"
import {
getAntiCSRFToken,
getPublicDataStore,
Expand All @@ -23,6 +24,7 @@ export interface BuildRpcClientParams {
resolverName: string
resolverType: ResolverType
routePath: string
httpMethod: string
}

export interface RpcOptions {
Expand Down Expand Up @@ -54,9 +56,10 @@ export function __internal_buildRpcClient({
resolverName,
resolverType,
routePath,
httpMethod,
}: BuildRpcClientParams): RpcClient {
const fullRoutePath = normalizeApiRoute("/api/rpc" + routePath)

const routePathURL = new URL(fullRoutePath, window.location.origin)
const httpClient: RpcClientBase = async (params, opts = {}, signal = undefined) => {
const debug = (await import("debug")).default("blitz:rpc")
if (!opts.fromQueryHook && !opts.fromInvoke) {
Expand Down Expand Up @@ -93,18 +96,26 @@ export function __internal_buildRpcClient({
serialized = serialize(params)
}

if (httpMethod === "GET") {
routePathURL.searchParams.set("params", stringify(serialized.json))
routePathURL.searchParams.set("meta", stringify(serialized.meta))
}

const promise = window
.fetch(fullRoutePath, {
method: "POST",
.fetch(routePathURL, {
method: httpMethod,
headers,
credentials: "include",
redirect: "follow",
body: JSON.stringify({
params: serialized.json,
meta: {
params: serialized.meta,
},
}),
body:
httpMethod === "POST"
? JSON.stringify({
params: serialized.json,
meta: {
params: serialized.meta,
},
})
: undefined,
signal,
})
.then(async (response) => {
Expand Down
41 changes: 29 additions & 12 deletions packages/blitz-rpc/src/index-server.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {assert, baseLogger, Ctx, newLine, prettyMs} from "blitz"
import {assert, baseLogger, Ctx, newLine, prettyMs, ResolverConfig} from "blitz"
import {NextApiRequest, NextApiResponse} from "next"
import {deserialize, serialize as superjsonSerialize} from "superjson"
import {deserialize, serialize as superjsonSerialize, parse} from "superjson"
import {resolve} from "path"
import chalk from "chalk"

Expand All @@ -14,6 +14,10 @@ function isObject(value: unknown): value is Record<string | symbol, unknown> {
return typeof value === "object" && value !== null
}

const defaultConfig: ResolverConfig = {
httpMethod: "POST",
}

function getGlobalObject<T extends Record<string, unknown>>(key: string, defaultValue: T): T {
assert(key.startsWith("__internal_blitz"), "unsupported key")
if (typeof global === "undefined") {
Expand All @@ -25,7 +29,7 @@ function getGlobalObject<T extends Record<string, unknown>>(key: string, default
}

type Resolver = (...args: unknown[]) => Promise<unknown>
type ResolverFiles = Record<string, () => Promise<{default?: Resolver}>>
type ResolverFiles = Record<string, () => Promise<{default?: Resolver; config?: ResolverConfig}>>
export type ResolverPathOptions = "queries|mutations" | "root" | ((path: string) => string)

// We define `global.__internal_blitzRpcResolverFiles` to ensure we use the same global object.
Expand All @@ -43,7 +47,7 @@ export function loadBlitzRpcResolverFilesWithInternalMechanism() {

export function __internal_addBlitzRpcResolver(
routePath: string,
resolver: () => Promise<{default?: Resolver}>,
resolver: () => Promise<{default?: Resolver; config?: ResolverConfig}>,
) {
g.blitzRpcResolverFilesLoaded = g.blitzRpcResolverFilesLoaded || {}
g.blitzRpcResolverFilesLoaded[routePath] = resolver
Expand Down Expand Up @@ -169,19 +173,33 @@ export function rpcHandler(config: RpcConfig) {
throw new Error("No resolver for path: " + routePath)
}

const resolver = (await loadableResolver()).default
const {default: resolver, config: resolverConfig} = await loadableResolver()

if (!resolver) {
throw new Error("No default export for resolver path: " + routePath)
}

const resolverConfigWithDefaults = {...defaultConfig, ...resolverConfig}

if (req.method === "HEAD") {
// We used to initiate database connection here
res.status(200).end()
return
} else if (req.method === "POST") {
// Handle RPC call

if (typeof req.body.params === "undefined") {
} else if (
req.method === "POST" ||
(req.method === "GET" && resolverConfigWithDefaults.httpMethod === "GET")
) {
if (req.method === "GET") {
if (Object.keys(req.query).length === 1 && req.query.blitz) {
const error = {message: "Request query is missing the required `params` and `meta` keys"}
log.error(error.message)
res.status(400).json({
result: null,
error,
})
return
}
} else if (typeof req.body.params === "undefined") {
const error = {message: "Request body is missing the `params` key"}
log.error(error.message)
res.status(400).json({
Expand All @@ -193,10 +211,9 @@ export function rpcHandler(config: RpcConfig) {

try {
const data = deserialize({
json: req.body.params,
meta: req.body.meta?.params,
json: req.method === "POST" ? req.body.params : parse(`${req.query.params}`),
meta: req.method === "POST" ? req.body.meta?.params : parse(`${req.query.meta}`),
})

log.info(customChalk.dim("Starting with input:"), data ? data : JSON.stringify(data))
const startTime = Date.now()
const result = await resolver(data, (res as any).blitzCtx)
Expand Down
17 changes: 16 additions & 1 deletion packages/blitz-rpc/src/loader-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {
toPosixPath,
} from "./loader-utils"
import {posix} from "path"
import {log, ResolverConfig} from "blitz"
import {getResolverConfig} from "./parse-rpc-config"

// Subset of `import type { LoaderDefinitionFunction } from 'webpack'`

Expand Down Expand Up @@ -39,12 +41,24 @@ export async function transformBlitzRpcResolverClient(
) {
assertPosixPath(id)
assertPosixPath(root)

const resolverFilePath = "/" + posix.relative(root, id)
assertPosixPath(resolverFilePath)
const routePath = convertPageFilePathToRoutePath(resolverFilePath, options?.resolverPath)
const resolverName = convertFilePathToResolverName(resolverFilePath)
const resolverType = convertFilePathToResolverType(resolverFilePath)
const resolverConfig: ResolverConfig = {
httpMethod: "POST",
}
if (resolverType === "query") {
try {
const {httpMethod} = getResolverConfig(_src)
if (httpMethod) {
resolverConfig.httpMethod = httpMethod
}
} catch (e) {
log.error(e as string)
}
}

const code = `
// @ts-nocheck
Expand All @@ -53,6 +67,7 @@ export async function transformBlitzRpcResolverClient(
resolverName: "${resolverName}",
resolverType: "${resolverType}",
routePath: "${routePath}",
httpMethod: "${resolverConfig.httpMethod}",
});
`

Expand Down
7 changes: 2 additions & 5 deletions packages/blitz-rpc/src/loader-server.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {dirname, join, posix, relative} from "path"
import {dirname, join, relative} from "path"
import {promises} from "fs"
import {
assertPosixPath,
Expand Down Expand Up @@ -52,18 +52,15 @@ export async function transformBlitzRpcServer(
assertPosixPath(root)

const blitzImport = 'import { __internal_addBlitzRpcResolver } from "@blitzjs/rpc";'

// No break line between `blitzImport` and `src` in order to preserve the source map's line mapping
let code = blitzImport + src
code += "\n\n"

for (let resolverFilePath of resolvers) {
const relativeResolverPath = slash(relative(dirname(id), join(root, resolverFilePath)))
const routePath = convertPageFilePathToRoutePath(resolverFilePath, options?.resolverPath)
code += `__internal_addBlitzRpcResolver('${routePath}', () => import('${relativeResolverPath}'));`
code += `__internal_addBlitzRpcResolver('${routePath}',() => import('${relativeResolverPath}'));`
code += "\n"
}

// console.log("NEW CODE", code)
return code
}
Expand Down
Loading

0 comments on commit ceb7db2

Please sign in to comment.