1
0
mirror of synced 2026-02-07 12:00:13 -05:00

Compare commits

..

2 Commits

Author SHA1 Message Date
Siddharth Suresh
b08c0ff49a use AST to parse the config and allow blitz server to be in src 2022-10-31 20:46:44 +05:30
Siddharth Suresh
ceb7db274f Add GET support to RPC specification (#3891) 2022-10-29 17:10:27 -04:00
15 changed files with 545 additions and 449 deletions

View File

@@ -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.

View File

@@ -11,3 +11,7 @@ export default async function getCurrentUser(_ = null, { session }: Ctx) {
return user
}
export const config = {
httpMethod: "GET",
}

View File

@@ -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",
}

View File

@@ -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
@@ -33,17 +30,6 @@ function runTests(dev = false) {
5000 * 60 * 2,
)
it(
"returns 404 for GET",
async () => {
const res = await fetchViaHTTP(appPort, "/api/rpc/getBasic", null, {
method: "GET",
})
expect(res.status).toEqual(404)
},
5000 * 60 * 2,
)
it(
"requires params",
async () => {
@@ -58,6 +44,71 @@ function runTests(dev = false) {
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(
"query works - GET",
async () => {
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 query is missing the required `params` and `meta` keys",
)
},
5000 * 60 * 2,
)
it(
"query works",
async () => {

View File

@@ -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",

View File

@@ -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,
@@ -23,6 +24,7 @@ export interface BuildRpcClientParams {
resolverName: string
resolverType: ResolverType
routePath: string
httpMethod: string
}
export interface RpcOptions {
@@ -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) {
@@ -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) => {

View File

@@ -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"
@@ -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") {
@@ -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.
@@ -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
@@ -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({
@@ -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)

View File

@@ -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'`
@@ -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
@@ -53,6 +67,7 @@ export async function transformBlitzRpcResolverClient(
resolverName: "${resolverName}",
resolverType: "${resolverType}",
routePath: "${routePath}",
httpMethod: "${resolverConfig.httpMethod}",
});
`

View File

@@ -1,4 +1,4 @@
import {dirname, join, posix, relative} from "path"
import {dirname, join, relative} from "path"
import {promises} from "fs"
import {
assertPosixPath,
@@ -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
}

View File

@@ -0,0 +1,57 @@
import {parseSync} from "@swc/core"
import {ResolverConfig} from "blitz"
type _ResolverType = "GET" | "POST"
const defaultResolverConfig: ResolverConfig = {
httpMethod: "POST",
}
export function getResolverConfig(content: string): ResolverConfig {
const resolverConfig = defaultResolverConfig
const resolver = parseSync(content, {
syntax: "typescript",
target: "es2020",
})
const exportDelaration = resolver.body.find((node) => {
if (node.type === "ExportDeclaration") {
if (node.declaration.type === "VariableDeclaration") {
if (node.declaration.declarations[0]?.id.type === "Identifier") {
if (node.declaration.declarations[0].id.value === "config") {
return true
}
}
}
}
return false
})
if (exportDelaration && exportDelaration.type == "ExportDeclaration") {
const declaration = exportDelaration.declaration
if (declaration && declaration.type == "VariableDeclaration") {
const declarator = declaration.declarations[0]
if (declarator && declarator.type == "VariableDeclarator") {
const variable = declarator.init
if (variable && variable.type == "ObjectExpression") {
const properties = variable.properties
if (properties) {
const httpMethodProperty = properties.find((property) => {
if (property.type == "KeyValueProperty") {
if (property.key.type == "Identifier") {
return property.key.value == "httpMethod"
}
}
return false
})
if (httpMethodProperty && httpMethodProperty.type == "KeyValueProperty") {
const value = httpMethodProperty.value
if (value && value.type == "StringLiteral") {
resolverConfig.httpMethod = value.value as _ResolverType
}
}
}
}
}
}
}
return resolverConfig
}

View File

@@ -15,7 +15,7 @@ import {
MutationsGenerator,
ModelGenerator,
QueryGenerator,
addCustomTemplatesBlitzConfig,
customTemplatesBlitzConfig,
} from "@blitzjs/generator"
import {log} from "../../logging"
@@ -65,7 +65,7 @@ const createCustomTemplates = async () => {
})
const templatesPathValue: string = templatesPath.value
const isTypeScript = await getIsTypeScript()
addCustomTemplatesBlitzConfig(templatesPathValue, isTypeScript)
await customTemplatesBlitzConfig(isTypeScript, templatesPathValue, true) // to run the codemod
log.success(`🚀 Custom templates path added/updated in app/blitz-server file`)
const customTemplatesPath = require("path").join(process.cwd(), templatesPathValue)
const fsExtra = await import("fs-extra")
@@ -275,20 +275,12 @@ const generate: CliCommand = async () => {
const generators = generatorMap[selectedType as keyof typeof generatorMap]
const isTypeScript = await getIsTypeScript()
const blitzServerPath = isTypeScript ? "app/blitz-server.ts" : "app/blitz-server.js"
const blitzServer = require("path").join(process.cwd(), blitzServerPath)
const {register} = require("esbuild-register/dist/node")
const {unregister} = register({
target: "es6",
})
const blitzConfig = require(blitzServer)
const {cliConfig} = blitzConfig
unregister()
const cliConfig = await customTemplatesBlitzConfig(isTypeScript)
for (const GeneratorClass of generators) {
const generator = new GeneratorClass({
destinationRoot: require("path").resolve(),
templateDir: cliConfig?.customTemplates,
templateDir: cliConfig,
extraArgs: args["_"].slice(3) as string[],
modelName: singularRootContext,
modelNames: modelNames(singularRootContext),

View File

@@ -6,6 +6,10 @@ export interface RouteUrlObject extends Pick<UrlObject, "pathname" | "query"> {
pathname: string
}
export type ResolverConfig = {
httpMethod: "GET" | "POST"
}
export type BlitzCliConfig = {
customTemplates?: string
}

View File

@@ -31,6 +31,7 @@
"diff": "5.0.0",
"enquirer": "2.3.6",
"fs-extra": "10.0.1",
"globby": "13.1.2",
"got": "^11.8.1",
"jscodeshift": "0.13.0",
"mem-fs": "1.2.0",

View File

@@ -16,12 +16,31 @@ import {readdirRecursive} from "./utils/readdir-recursive"
import prettier from "prettier"
const debug = require("debug")("blitz:generator")
export const addCustomTemplatesBlitzConfig = (
customTemplatesPath: string,
export function getProjectRootSync() {
return path.dirname(getConfigSrcPath())
}
export function getConfigSrcPath() {
const jsPath = path.resolve(path.join(process.cwd(), "next.config.js"))
return jsPath
}
export const customTemplatesBlitzConfig = async (
isTypeScript: boolean,
customTemplatesPath = "",
codemod = false,
) => {
const blitzServer = isTypeScript ? "app/blitz-server.ts" : "app/blitz-server.js"
const blitzServerPath = require("path").join(process.cwd(), blitzServer)
const {globby} = await import("globby")
const blitzServer = await globby(["{app,src}/**/blitz-server.{ts,js}"], {
cwd: getProjectRootSync(),
})
if (blitzServer.length === 0) {
throw new Error("Could not find blitz-server.js or blitz-server.ts in app or src folder")
}
if (blitzServer.length > 1) {
throw new Error("Found more than one blitz-server.js or blitz-server.ts in app or src folder")
}
const blitzServerPath = require("path").join(process.cwd(), blitzServer.at(0))
const userConfigModuleSource = fs.readFileSync(blitzServerPath, {encoding: "utf-8"})
const userConfigModule = j(userConfigModuleSource, {parser: customTsParser})
const program = userConfigModule.get()
@@ -86,6 +105,9 @@ export const addCustomTemplatesBlitzConfig = (
if (customTemplatesProperty.type === "ObjectProperty") {
const customValue = customTemplatesProperty.value
if (customValue.type === "StringLiteral") {
if (!codemod) {
return customValue.value
}
customValue.value = customTemplatesPath
}
}
@@ -94,8 +116,10 @@ export const addCustomTemplatesBlitzConfig = (
}
}
}
const newSource = userConfigModule.toSource()
fs.writeFileSync(blitzServerPath, newSource)
if (codemod) {
const newSource = userConfigModule.toSource()
fs.writeFileSync(blitzServerPath, newSource)
}
}
export const customTsParser = {

679
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff